从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC)

从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC)

### 文章目录

  • [从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [@[TOC]](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [1. 什么是 SPI?硬件信号与连接![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/bb66c86932dd4437ab05ddf4c8a8eedb.png)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [2. SPI 四种模式(CPOL / CPHA)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [3. Linux SPI 子系统框架](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [4. 设备树中如何描述 SPI 设备](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [5. 最简单的 SPI 字符设备驱动框架](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [6. 实战:TLC5615 DAC 驱动完整编写![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55b946b759e74c2baeb038874d3efc9b.png)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [6.1 TLC5615 芯片及数据格式](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [6.2 完整驱动代码(含注释)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [6.3 驱动代码关键点解析](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [7. 编译、装载与测试](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [7.1 编译驱动](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [7.2 更新设备树](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [7.3 装载驱动与测试](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [8. 常见问题与内核错误分析](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [8.1 错误:`cannot set clock freq: 2 (base freq: 60000000)`](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [8.2 内核 `paging request` 崩溃](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9 完整流程框架(以写入数值 200 为例)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9.1 用户空间程序 `dac_test` 工作](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9.2 C 库到系统调用](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9.3 VFS 层找到对应的字符设备驱动](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9.4 驱动 `spi_drv_write` 内部执行细节](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9.5内核 SPI 核心层(`spi_sync_transfer`)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9.6SPI 控制器驱动(硬件操作)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9.7 回到驱动层及用户空间](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [9.8 硬件端 TLC5615 响应](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [10. 总结与面试自测题](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))
  • [面试自测题(附答案)](#文章目录 从零开始学习 Linux SPI 驱动开发(基于 IMX6ULL + TLC5615 DAC) @[TOC] 1. 什么是 SPI?硬件信号与连接 2. SPI 四种模式(CPOL / CPHA) 3. Linux SPI 子系统框架 4. 设备树中如何描述 SPI 设备 5. 最简单的 SPI 字符设备驱动框架 6. 实战:TLC5615 DAC 驱动完整编写 6.1 TLC5615 芯片及数据格式 6.2 完整驱动代码(含注释) 6.3 驱动代码关键点解析 7. 编译、装载与测试 7.1 编译驱动 7.2 更新设备树 7.3 装载驱动与测试 8. 常见问题与内核错误分析 8.1 错误:cannot set clock freq: 2 (base freq: 60000000) 8.2 内核 paging request 崩溃 9 完整流程框架(以写入数值 200 为例) 9.1 用户空间程序 dac_test 工作 9.2 C 库到系统调用 9.3 VFS 层找到对应的字符设备驱动 9.4 驱动 spi_drv_write 内部执行细节 9.5内核 SPI 核心层(spi_sync_transfer) 9.6SPI 控制器驱动(硬件操作) 9.7 回到驱动层及用户空间 9.8 硬件端 TLC5615 响应 10. 总结与面试自测题 面试自测题(附答案))

1. 什么是 SPI?硬件信号与连接

SPI(Serial Peripheral Interface)是一种全双工、同步串行总线,由摩托罗拉提出。基本信号有 4 根线:

信号名 方向(相对于主控) 作用
SCK 主 → 从 时钟信号,由主机产生
MOSI 主 → 从 主机发送,从机接收(Master Out Slave In)
MISO 主 ← 从 从机发送,主机接收(Master In Slave Out)
CS/SS 主 → 从 片选信号,低有效,选中某个从设备

你的板子上已经引出了这些信号

  • J2 上标注了 SPI1 MOSISPI1 MISOSPI1 SCLKSPI1 CS0
  • J7 的 19、20、21、22 脚也是 SPI1 SCLKSPI1 CS0SPI1 MOSISPI1 MISO

这意味着你的 IMX6ULL 开发板通过排针把 SPI1 控制器的引脚都引出来了,你可以直接拿杜邦线外接 SPI 设备(比如这次要驱动的 TLC5615 DAC 小板子)。

通信过程概括

主机拉低 CS 选中从机,然后产生时钟。在每个时钟边沿,主机从 MOSI 线移出一位数据,同时从 MISO 线移入一位数据。传输完一个或多个字节后,主机拉高 CS 结束会话。


2. SPI 四种模式(CPOL / CPHA)

SPI 没有官方标准,不同从设备对时钟极性和相位的要求不一样,于是有了 4 种模式,由两个参数决定:

  • CPOL(时钟极性)
    • CPOL=0:空闲时 SCK 为低电平
    • CPOL=1:空闲时 SCK 为高电平
  • CPHA(时钟相位)
    • CPHA=0:在第一个时钟边沿采样数据
    • CPHA=1:在第二个时钟边沿采样数据

组合起来:

模式 CPOL CPHA 空闲 SCK 采样边沿
0 0 0 上升沿
1 0 1 下降沿
2 1 0 下降沿
3 1 1 上升沿

在 Linux 设备树或 spi_board_info 中,通过 spi-cphaspi-cpol 属性来指定。例如:

dts

复制代码
spi-cpha;
spi-cpol;

不加时默认模式 0。TLC5615 数据手册要求 CPOL=0, CPHA=1(即模式 1)吗? 其实很多 DAC 只是要求在 SCK 上升沿移入数据,需要查手册。实验中如果不稳定,很可能就是模式没配对。我们的例子里暂未加这两个属性,内核会按模式 0 工作,但为了严谨,应该根据芯片手册填写。


3. Linux SPI 子系统框架

Linux 把 SPI 架构分成三层,像搭积木一样:

  1. SPI 控制器驱动spi_master 或新版本叫 spi_controller
    直接与 SoC 的硬件 SPI 外设打交道。像 spi_imx 就是 IMX6ULL 的 SPI 控制器驱动,已经在内核里写好了,我们不用管。
  2. SPI 设备struct spi_device
    描述一个挂载在 SPI 总线上的从设备。它保存着该设备的片选索引、最大频率、模式等。这些信息主要来自设备树。
  3. SPI 设备驱动struct spi_driver
    我们写的驱动,负责与具体的从设备交互。内核通过 compatible 属性把它和设备树中的节点绑定起来。

一次 SPI 数据传输的核心数据结构

  • struct spi_transfer:描述一次传输的细节(发送缓冲区、接收缓冲区、长度、速度等)。
  • struct spi_message:将多个 spi_transfer 链接成一个原子操作,在全部传输完成后才释放 CS。
  • spi_sync_transfer(spi, xfers, num):同步接口,提交传输并阻塞等待完成。这是我们驱动中最常用的函数。

辅助函数:

c

复制代码
static inline int spi_read(struct spi_device *spi, void *buf, size_t len)
{
    struct spi_transfer t = {
        .rx_buf = buf,
        .len    = len,
    };
    return spi_sync_transfer(spi, &t, 1);
}

就是一个只读的同步封装,简单明了。


4. 设备树中如何描述 SPI 设备

看你的设备树片段(arch/arm/boot/dts/100ask_imx6ull-14x14.dts):

dts

复制代码
&ecspi1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ecspi1>;
    fsl,spi-num-chipselects = <2>;
    cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>, <&gpio4 24 GPIO_ACTIVE_LOW>;
    status = "okay";

    dac: dac {
        compatible = "100ask,spidev";
        reg = <0>;
        spi-max-frequency = <1000000>;
    };
};

逐行解释

  • &ecspi1:引用 SPI1 控制器节点。
  • pinctrl-0 = <&pinctrl_ecspi1>;:指定引脚复用为 SPI 功能(已在别处定义)。
  • cs-gpios:指定两个片选引脚,分别是 GPIO4_26 和 GPIO4_24,低有效。控制器会根据 reg 编号自动选择对应 GPIO。
  • status = "okay";:启用该控制器。
  • 子节点 dac:代表一个挂在 ecspi1 上的 SPI 设备。reg = <0> 表示使用第 0 个片选(即 GPIO4_26)。spi-max-frequency = <1000000> 限制最大时钟为 1 MHz。
  • compatible = "100ask,spidev";这是最关键的匹配字符串 。内核会用它与所有 spi_driverof_match_table 比较,相等时就会调用驱动的 probe 函数,并把该节点生成的 spi_device 传进去。

你终端里执行 ls /sys/bus/spi/devices/spi0.0 能看到 drivermodaliasof_node 等,说明设备 spi0.0 已正确创建并与驱动绑定。


5. 最简单的 SPI 字符设备驱动框架

我们不只是让内核能识别设备,还要让用户空间程序(比如 dac_test)能打开、写入、读取 这个 SPI 设备。套路是:probe 中注册一个字符设备,生成 /dev/myspi 节点

结构体骨架:

c

复制代码
static struct spi_driver my_spi_driver = {
    .driver = {
        .name = "100ask_spi_drv",
        .owner = THIS_MODULE,
        .of_match_table = myspi_dt_match,   // 与设备树 compatible 匹配
    },
    .probe  = spi_drv_probe,
    .remove = spi_drv_remove,
};

probe 函数完成三件事:

  1. 保存 spi_device * 指针(通常放到全局变量或 spi_set_drvdata)。
  2. 申请字符设备号,绑定 file_operations
  3. 创建设备类并在 /dev 下生成节点。

你没贴出来的 file_operations 里面,read / write 就是最终与硬件通信的地方。


6. 实战:TLC5615 DAC 驱动完整编写

6.1 TLC5615 芯片及数据格式

TLC5615 是一个 10 位 DAC,它接受 12 位或 16 位输入序列。图片给出了格式:

  • 12 位模式:高 10 位是数据,最低 2 位是无用位(sub-LSB)。
  • 16 位模式:高 4 位无意义,接着 10 位数据,最后 2 位 sub-LSB 无用位。

我们要驱动它输出一个模拟电压,只需要给它发送合适的数字值即可。为了方便,我们可以固定使用 16 位模式,即发送两个字节,高 4 位随便填(但一般会考虑到对齐),后面跟着左移后的 10 位数据。

在老师提供的驱动代码 write 函数里,数据转换如下:

c

复制代码
err = copy_from_user(&val, buf, size);   // 得到用户空间的 16 位值(实际只用到低 10 位)
val <<= 2;      // 数据左移 2 位,把 10 位数据挪到 D11..D2 位置,低 2 位为 sub-LSB
val &= 0x0fff;  // 屏蔽高 4 位,确保发送 12 位有效数据

为何 val <<= 2

因为 10 位数据在芯片的 16 位帧里位于 "4 dummy bits + 10 data bits + 2 extra bits" 结构。如果我们把 10 位数据放在一个 16 位字的高 10 位,val << 2 就是把它移到 D15...D6 吗? 不,实际上代码里:

  • valunsigned shortcopy_from_user(&val, buf, 2) 得到的是原始的用户值。
  • 左移 2 位再与 0x0fff 相与,结果是一个 12 位的值,其高 10 位是数据,低 2 位为 0。
  • 再把它拆成两个字节发送:ker_buf[0] = val >> 8; ker_buf[1] = val;
    那么对于 16 位帧来说,我们发送了两个字节共 16 位:高字节是 val 的高 8 位,低字节是 val 低 8 位。结果就是前 4 位为 0(因为 val & 0x0fff 清除了高 4 位),接着 10 位数据,最后 2 位为 0。这完全符合 16 位输入序列格式。

6.2 完整驱动代码(含注释)

下面是整合了老师源码的完整驱动程序,我将 write 方法配上详尽注释,并实现一个简单的 read(DAC 通常不需要读,这里返回错误)。你可以直接使用。

c

复制代码
#include <linux/spi/spi.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

static int major = 0;
static struct class *my_spi_class;
static struct spi_device *g_spi;

static ssize_t spi_drv_write(struct file *file, const char __user *buf,
                             size_t size, loff_t *offset)
{
    int err;
    unsigned short val;      // 用户写来的值, 2 字节
    unsigned char ker_buf[2]; // 内核中将要发送的 2 字节
    struct spi_transfer t;

    // 我们约定一次必须写入 2 字节 (一个 DAC 数值)
    if (size != 2)
        return -EINVAL;

    // 从用户空间读取 2 字节到 val
    err = copy_from_user(&val, buf, size);
    if (err)
        return -EFAULT;

    /*
     * TLC5615 数据格式(16 位模式):
     * 高 4 位: 无关位
     * 接着 10 位: 有效数据 (D9~D0)
     * 最低 2 位: sub-LSB 位 (通常填 0)
     *
     * 我们先把用户给的 val (假设其低 10 位有效) 左移 2 位,
     * 使 10 位数据位于 12 位字段的高 10 位,然后清除高 4 位。
     * 最终 val 是一个 12 位的值,再分为两个字节发送,结果:
     * 字节0: 0000xxxx (前4位为0)  字节1: xxxxxx00 (后2位为0)
     * 满足芯片要求。
     */
    val <<= 2;          // 10-bit data → D11..D2
    val &= 0x0fff;      // 屏蔽高4位,确保前4位为0

    ker_buf[0] = (val >> 8) & 0xff;  // 高字节
    ker_buf[1] = val & 0xff;         // 低字节

    memset(&t, 0, sizeof(t));
    t.tx_buf = ker_buf;
    t.len    = 2;

    err = spi_sync_transfer(g_spi, &t, 1);
    if (err) {
        pr_err("spi_sync_transfer failed: %d\n", err);
        return err;
    }

    return size;
}

static ssize_t spi_drv_read(struct file *file, char __user *buf,
                            size_t size, loff_t *offset)
{
    // TLC5615 是纯写入设备,不支持读取,直接返回错误
    return -ENOTSUP;
}

static int spi_drv_open(struct inode *inode, struct file *file)
{
    return 0;
}

static int spi_drv_release(struct inode *inode, struct file *file)
{
    return 0;
}

static const struct file_operations spi_drv_fops = {
    .owner   = THIS_MODULE,
    .open    = spi_drv_open,
    .release = spi_drv_release,
    .write   = spi_drv_write,
    .read    = spi_drv_read,
};

static int spi_drv_probe(struct spi_device *spi)
{
    g_spi = spi;   // 保存 spi_device,供读写使用

    major = register_chrdev(0, "100ask_spi", &spi_drv_fops);
    if (major < 0) {
        pr_err("Failed to register chrdev\n");
        return major;
    }

    my_spi_class = class_create(THIS_MODULE, "100ask_spi_class");
    if (IS_ERR(my_spi_class)) {
        unregister_chrdev(major, "100ask_spi");
        return PTR_ERR(my_spi_class);
    }

    device_create(my_spi_class, NULL, MKDEV(major, 0), NULL, "myspi");
    pr_info("myspi device created, major=%d\n", major);
    return 0;
}

static int spi_drv_remove(struct spi_device *spi)
{
    device_destroy(my_spi_class, MKDEV(major, 0));
    class_destroy(my_spi_class);
    unregister_chrdev(major, "100ask_spi");
    return 0;
}

static const struct of_device_id myspi_dt_match[] = {
    { .compatible = "100ask,spidev" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, myspi_dt_match);

static struct spi_driver my_spi_driver = {
    .driver = {
        .name           = "100ask_spi_drv",
        .owner          = THIS_MODULE,
        .of_match_table = myspi_dt_match,
    },
    .probe  = spi_drv_probe,
    .remove = spi_drv_remove,
};

module_spi_driver(my_spi_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("YourName");
MODULE_DESCRIPTION("SPI DAC driver for TLC5615");

6.3 驱动代码关键点解析

  • 模块入口module_spi_driver(my_spi_driver); 是宏,自动生成 module_initmodule_exit,里面调用 spi_register_driverspi_unregister_driver。比手写更简洁。
  • compatible 匹配 :设备树里有 compatible = "100ask,spidev";,驱动 of_match_table 包含同样的字符串,所以它们会绑定。
  • 字符设备注册 :老用法 register_chrdev 一次占用 256 个次设备号,简单够用。主设备号动态分配(传入 0)。
  • 数据传输spi_sync_transfer 第一个参数是 g_spi,它指向该设备对应的 spi_device。内核自动使用设备树中指定的 spi-max-frequency、片选等。我们不需要手动控制 CS,核心层会帮我们开关 cs-gpios

7. 编译、装载与测试

7.1 编译驱动

把上述代码保存为 spi_drv.c,在同一目录编写 Makefile

makefile

复制代码
KERN_DIR = /path/to/your/kernel/source   # 例如 ~/100ask_imx6ull-sdk/Linux-4.9.88
obj-m += spi_drv.o
all:
	make -C $(KERN_DIR) M=$(PWD) modules
clean:
	make -C $(KERN_DIR) M=$(PWD) clean

然后执行 make,生成 spi_drv.ko

7.2 更新设备树

根据操作截图:

  1. 编辑 arch/arm/boot/dts/100ask_imx6ull-14x14.dts,在 &ecspi1 内加入 dac 节点(你已添加)。
  2. 在内核源码目录执行 make dtbs
  3. 将新生成的 100ask_imx6ull-14x14.dtb 复制到 /boot 目录或开发板的启动分区。
  4. 重启开发板。

7.3 装载驱动与测试

在开发板终端:

bash

复制代码
insmod spi_drv.ko

查看是否成功生成 /dev/myspi

bash

复制代码
ls -l /dev/myspi

你的截图里还能看到 /sys/bus/spi/devices/spi0.0,以及dac_test 应用程序。dac_test 应该是接收两个参数:/dev/myspi 和 一个数值,例如:

bash

复制代码
./dac_test /dev/myspi 100

这会让 DAC 输出与数字 100 对应的模拟电压。你那几条执行记录:

bash

复制代码
./dac_test /dev/myspi 1000

bash

复制代码
./dac_test /dev/myspi 500

bash

复制代码
./dac_test /dev/myspi 200

说明驱动工作正常,能多次写入不同值控制电压(后面 LED 亮度会变化)。


8. 常见问题与内核错误分析

8.1 错误:cannot set clock freq: 2 (base freq: 60000000)

你截图里出现过:

text

复制代码
spi_imx 2008000.ecspi: cannot set clock freq: 2 (base freq: 60000000)

这是因为你传给驱动的最大频率可能太小(比如 2 Hz),或者某个 spi_transferspeed_hz 设得不成比例。但你的设备树里 spi-max-frequency = <1000000>(1 MHz),不应出现这个错误。可能原因:

  • 之前测试时修改过频率但没有更新 dtb。
  • 或者驱动程序里额外设置了 t.speed_hz = 2; 之类。
    解决办法:检查设备树频率,确保不要极端值;spi-max-frequency 取 100000 ~ 按芯片最大能力。

8.2 内核 paging request 崩溃

你图中:

text

复制代码
Unable to handle kernel paging request at virtual address 8010e710

常见原因:访问了非法指针,比如 g_spi 还是 NULL 时就调用了 spi_sync_transfer,或者 copy_from_user 的缓冲区不合法。确保驱动在 probe 之后才被读写,并且 g_spi 被正确赋值。


9 完整流程框架(以写入数值 200 为例)

9.1 用户空间程序 dac_test 工作

  • 解析命令行参数:argv[1] = "/dev/myspi"argv[2] = "200"
  • 调用 open("/dev/myspi", O_WRONLY) 打开设备文件。
  • 将字符串 "200" 转换为整数 200atoistrtol)。
  • 准备 2 字节数据:因为 TLC5615 是 10 位 DAC,用户程序可能直接将 200 当成 16 位无符号数写入,也就是准备 unsigned short val = 200;,然后调用 write(fd, &val, 2) 向设备写入 2 个字节。

注:你的截图中 ./dac_test /dev/myspi 100 等用法与上述逻辑一致。

9.2 C 库到系统调用

  • write(fd, &val, 2) 触发 SYS_write 系统调用 ,陷入内核(ARM 上通过 swi / svc 指令)。
  • 内核根据系统调用号进入 sys_write(),然后调用 vfs_write()

9.3 VFS 层找到对应的字符设备驱动

  • vfs_write()fd 对应的 struct file 中取出 file->f_op,即 spi_drv_fops
  • 调用 file->f_op->write(file, buf, count, &pos),也就是我们的 spi_drv_write
  • buf 指向用户空间栈上的 val 地址(需要 copy_from_user),count 为 2。

9.4 驱动 spi_drv_write 内部执行细节

c

复制代码
unsigned short val;
unsigned char ker_buf[2];
struct spi_transfer t;
  • 长度检查size != 2 → 返回 -EINVAL(这里为 2,通过)。

  • 拷贝用户数据copy_from_user(&val, buf, 2) 将用户空间的两个字节复制到内核变量 val。此时 val = 200(无论主机字节序,内核内部视为 16 位无符号数)。

  • 数据格式转换(符合 TLC5615 的 16 位帧要求):

    • val <<= 2;200 << 2 = 800(二进制:0000 0011 0010 0000,十六进制 0x320
    • val &= 0x0fff;0x320 & 0xfff = 0x320(确保只保留低 12 位,防止溢出)
    • 此时 12 位值 0x320 的组成:高 4 位 0x3 是 dummy 位(无关位),中间 10 位 0x320 >> 2 = 200 是有效数据,低 2 位为 0(sub-LSB)
  • 拆分为两个字节

    • ker_buf[0] = (val >> 8) & 0xff;0x03
    • ker_buf[1] = val & 0xff;0x20
  • 构造 SPI 传输

    c

    复制代码
    memset(&t, 0, sizeof(t));
    t.tx_buf = ker_buf;   // 发送缓冲区
    t.len    = 2;         // 发送 2 字节
  • 发起同步 SPI 传输

    c

    复制代码
    err = spi_sync_transfer(g_spi, &t, 1);
    • g_spi 是在 probe 中保存的 spi_device 指针,它包含了从设备树继承的片选号、最大频率等信息。
    • 如果发送成功,返回 0;发生的字节数 size(2)返回给用户空间。

9.5内核 SPI 核心层(spi_sync_transfer

  • spi_sync_transfer 内部创建一个 spi_message,将 spi_transfer 添加进去,然后调用 spi_sync(spi, &message)
  • spi_sync 为此次传输设置一个等待队列,然后将 spi_message 提交给 SPI 控制器驱动,并阻塞等待传输完成。
  • 实际的传输由 struct spi_controllertransfer_one_message 方法完成,这里对应 spi_imx 驱动(IMX6ULL 的 SPI 控制器驱动)。

9.6SPI 控制器驱动(硬件操作)

  • 片选控制 :控制器驱动根据 spi_device 的片选信息(来自设备树 cs-gpios = <&gpio4 26 ...>;)将 GPIO4_26 拉低,选中 TLC5615。
  • 配置时钟 :根据设备树中的 spi-max-frequency = <1000000> 配置 SCK 为 1 MHz,并根据模式 0(未添加 spi-cpha/cpol)设置空闲低电平、上升沿采样等。
  • 启动传输
    • ker_buf 的两个字节(0x03, 0x20)通过 MOSI 引脚顺序移出。
    • 控制器自动产生 16 个时钟脉冲,每当时钟边沿到来时,发送一位数据。
    • 由于本次只写不收,MISO 线上的数据被忽略(控制器不会将接收到的数据存入任何缓冲区,或者即使接收也不处理)。
  • 等待完成:硬件发送完所有位后触发中断,中断服务程序通知 SPI 核心传输结束。
  • 释放片选:控制器将 GPIO4_26 拉高,结束本次 SPI 会话。
  • spi_sync_transfer 被唤醒,返回成功状态。

9.7 回到驱动层及用户空间

  • spi_drv_write 获得 err == 0,返回 size(2)。
  • vfs_write 将返回值 2 一路返回给用户空间的 write() 调用。
  • 用户程序 dac_test 收到 write 返回 2,之后可能调用 close(fd) 关闭设备。

9.8 硬件端 TLC5615 响应

  • TLC5615 在 16 个时钟周期内收到了完整的一帧 16 位数据:0x030x20
  • 根据芯片数据手册,它解析出 10 位有效数据 200,同时忽略高 4 位和低 2 位。
  • 内部 DAC 寄存器更新,模拟输出电压变为 Vout = Vref × (200 / 1024)
  • 你实验板上的 LED 亮度随之变化,因为 LED 接在 DAC 输出端(你的截图备注"DAC效果展示LED灯")。

整个链条总结为

用户敲命令 → 用户程序写设备节点 → 系统调用陷入内核 → VFS 调用驱动 write → 驱动转换数据并调用 spi_sync_transfer → SPI 核心阻塞等待 → IMX SPI 控制器拉低 CS、产生时钟、发送两字节 → 完成后拉高 CS、唤醒等待 → write 返回 → 用户程序退出 → DAC 芯片更新电压输出。

任何一环出问题,都可以对照这个框架进行精准排查。

10. 总结与面试自测题

这篇教程带你走完了从 SPI 物理总线到底层驱动的全过程:

  • 认识了四根 SPI 信号线和硬件连接。
  • 理解了 CPOL/CPHA 决定的四种模式。
  • 掌握了 Linux SPI 子系统的分层模型,关键结构体 spi_devicespi_driverspi_transfer
  • 学会了在设备树中添加 SPI 从设备节点,并通过 compatible 与驱动绑定。
  • 完成了字符设备驱动的编写,使用 spi_sync_transfer 发送数据。
  • 分析了 TLC5615 的数据格式,并在 write 函数中实现了位操作转换。
  • 最后在开发板上实际装载并成功控制 DAC 输出。

面试自测题(附答案)

1. SPI 总线有几根线?分别是什么?

答:4 根,SCK(时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选,通常低有效)。

2. 什么是 CPOL 和 CPHA?它们在设备树中如何指定?

答:CPOL 定义空闲时钟电平(0 低 1 高),CPHA 定义数据采样边沿(0 第一边沿,1 第二边沿)。设备树中通过 spi-cpolspi-cpha 布尔属性设置。

3. 在 Linux SPI 子系统中,spi_transferspi_message 的关系是什么?

答:spi_transfer 描述单次传输(TX/RX 缓冲区、长度)。spi_message 是多个 spi_transfer 的集合,保证它们被原子地执行(CS 在整条消息期间保持有效)。

4. 设备树中 compatible 属性有什么作用?

答:它告诉内核设备的型号,内核用它来匹配对应的驱动程序。驱动程序通过 of_match_table 声明自己支持的 compatible 列表,匹配成功后调用 probe 函数。

5. 驱动中的 probe 函数主要做哪些事情?

答:①获取并保存 spi_device;②初始化硬件(如配置时钟、复位);③注册字符设备/创建 sysfs 接口;④创建设备节点,让用户空间可以操作。

6. copy_from_usercopy_to_user 为什么是必需的?

答:因为用户空间和内核空间的内存是隔离的,不能直接解引用用户指针。这两个函数会处理地址合法性检查和缺页异常,安全地拷贝数据。

7. 如果板子上有两个同样的 SPI 设备,分别挂在 CS0 和 CS1,驱动应该怎么做才能同时支持?

答:不能在驱动中用单一全局变量 g_spi 指向设备。应当将 spi_device 填到 fileprivate_data 或者用 spi_set_drvdatacontainer_ofspi_device 获取私有结构。每次 open 时根据次设备号或 inode 定位到对应的 spi_device,避免覆盖。

8. TLC5615 的 12 位帧与 16 位帧在使用上有何区别?代码中 val <<= 2; val &= 0x0fff; 换成 val <<= 4 再发两个字节会怎样?

答:本驱动直接按 16 位帧发送,代码中的移位和掩码实现了高 4 位为 0、接着 10 位数据、低 2 位为 0 的正确帧格式。如果改成 val <<= 4,会导致数据偏移到更高位,超出 12 位范围,发送的时序不符合芯片要求,输出电压会出错。

相关推荐
feng_you_ying_li1 小时前
linux之进程控制
linux
Mr_pyx1 小时前
CompletableFuture 使用全攻略:从异步编程到异常处理
linux·前端·python
时空自由民.2 小时前
嵌入式学习-构建系统(图形化IDE/Kconfig/手动makefile Cmake)
数据库·ide·单片机·学习
2301_780943842 小时前
第二阶段:Gem5基础学习
学习
拾贰_C2 小时前
【OpenAI | Ubuntu | bigmodel】 openai规范配置bigmodel(zhipu)大模型api
linux·运维·ubuntu
计算机安禾2 小时前
【Linux从入门到精通】第22篇:Shell变量与数据类型——数字与字符串处理
linux·运维·chrome
我想我不够好。2 小时前
坚持到习惯为止
学习·学习方法
idolao2 小时前
CentOS 7 安装 jprofiler_linux64_7_2_3.tar.gz 详细步骤(解压、配置、远程连接)
linux·python·centos
深邃-2 小时前
【Web安全】-Kali,Linux配置(1):Kali网络配置,LinuxEnvConfig配置脚本,APT源的讲解,Kali设置中文
linux·运维·开发语言·网络·安全·web安全·网络安全