Linux驱动-SPI子系统
2024年10月 · 预计阅读时间: 12 分钟
参考《百问网-驱动大全》、《【北京迅为】itop-3568 开发板驱动开发指南》。
#
1.SPI 总线设备驱动模型#
1.1 数据结构内核文件参考:include\linux\spi\spi.h
#
(1)SPI 控制器Linux 中使用spi_master
结构体描述 SPI 控制器,里面最重要的成员就是transfer
函数指针:
#
(2)SPI 设备Linux 中使用spi_device
结构体描述 SPI 设备,里面记录有设备的片选引脚、频率、挂在哪个 SPI 控制器下面:
#
(3)SPI 设备驱动Linux 中使用spi_driver
结构体描述 SPI 设备驱动:
#
1.2 SPI 驱动框架我们可以先看下平台总线驱动模型,都是类似的思想,将外部设备资源与驱动分开:
SPI 驱动框架:
#
1.3 SPI 控制器驱动程序SPI 控制器的驱动程序可以基于”平台总线设备驱动”模型来实现:
- 在设备树里描述 SPI 控制器的硬件信息,在设备树子节点里描述挂在下面的 SPI 设备的信息
- 在 platform_driver 中提供一个 probe 函数
- 它会注册一个
spi_master
- 还会解析设备树子节点,创建
spi_device
结构体
- 它会注册一个
#
1.4 SPI 设备驱动程序跟”平台总线设备驱动模型”类似,Linux 中也有一个”SPI 总线设备驱动模型”:
- 左边是 spi_driver,使用 C 文件实现,里面有 id_table 表示能支持哪些 SPI 设备,有 probe 函数
- 右边是 spi_device,用来描述 SPI 设备,比如它的片选引脚、频率
- 可以来自设备树:比如由 SPI 控制器驱动程序解析设备树后创建、注册 spi_device
- 可以来自 C 文件:比如使用
spi_register_board_info
创建、注册 spi_device
#
2.SPI 设备树主要就是定义 SPI 控制器与 SPI 设备,在 SPI 控制器驱动程序中就会解析设备树,注册二者。
各个成员含义如下:
max_speed_hz
:该设备能支持的 SPI 时钟最大值chip_select
:是这个 spi_master 下的第几个设备- 在 spi_master 中有一个 cs_gpios 数组,里面存放有下面各个 spi 设备的片选引脚
- spi_device 的片选引脚就是:cs_gpios[spi_device.chip_select]
cs_gpio
:这是可选项,也可以把 spi_device 的片选引脚记录在这里bits_per_word
:每个基本的 SPI 传输涉及多少位- word:我们使用 SPI 控制器时,一般是往某个寄存器里写入数据,SPI 控制器就会把这些数据一位一位地发送出去
- 一个寄存器是 32 位的,被称为一个 word(有时候也称为 double word)
- 这个寄存器里多少位会被发送出去?使用 bits_per_word 来表示
- 扩展:bits_per_word 是可以大于 32 的,也就是每次 SPI 传输可能会发送多于 32 位的数据,这适用于 DMA 突发传输
mode
:含义广泛,看看结构体里那些宏SPI_CPHA
:在第 1 个周期采样,在第 2 个周期采样?SPI_CPOL
:平时时钟极性- SPI_CPHA 和 SPI_CPOL 组合起来就可以得到 4 种模式
- SPI_MODE_0:平时 SCK 为低(SPI_CPOL 为 0),在第 1 个周期采样(SPI_CPHA 为 0)
- SPI_MODE_1:平时 SCK 为低(SPI_CPOL 为 0),在第 2 个周期采样(SPI_CPHA 为 1)
- SPI_MODE_2:平时 SCK 为高(SPI_CPOL 为 1),在第 1 个周期采样(SPI_CPHA 为 0)
- SPI_MODE_3:平时 SCK 为高(SPI_CPOL 为 1),在第 2 个周期采样(SPI_CPHA 为 1)
SPI_CS_HIGH
:一般来说片选引脚时低电平有效,SPI_CS_HIGH 表示高电平有效SPI_LSB_FIRST
:- 一般来说先传输 MSB(最高位),SPI_LSB_FIRST 表示先传 LSB(最低位);
- 很多 SPI 控制器并不支持 SPI_LSB_FIRST
SPI_3WIRE
:SO、SI 共用一条线SPI_LOOP
:回环模式,就是 SO、SI 连接在一起SPI_NO_CS
:只有一个 SPI 设备,没有片选信号,也 6 不需要片选信号SPI_READY
:SPI 从设备可以拉低信号,表示暂停、表示未就绪SPI_TX_DUAL
:发送数据时有 2 条信号线SPI_TX_QUAD
:发送数据时有 4 条信号线SPI_RX_DUAL
:接收数据时有 2 条信号线SPI_RX_QUAD
:接收数据时有 4 条信号线
#
2.1 SPI Master在设备树中,对于 SPI Master,必须的属性如下:
- #address-cells:这个 SPI Master 下的 SPI 设备,需要多少个 cell 来表述它的片选引脚
- #size-cells:必须设置为 0
- compatible:根据它找到 SPI Master 驱动
可选的属性如下:
- cs-gpios:SPI Master 可以使用多个 GPIO 当做片选,可以在这个属性列出那些 GPIO
- num-cs:片选引脚总数
其他属性都是驱动程序相关的,不同的 SPI Master 驱动程序要求的属性可能不一样。
#
2.2 SPI Device在 SPI Master 对应的设备树节点下,每一个子节点都对应一个 SPI 设备,这个 SPI 设备连接在该 SPI Master 下面。
这些子节点中,必选的属性如下:
- compatible:根据它找到 SPI Device 驱动
- reg:用来表示它使用哪个片选引脚
- spi-max-frequency:必选,该 SPI 设备支持的最大 SPI 时钟
可选的属性如下:
- spi-cpol:这是一个空属性(没有值),表示 CPOL 为 1,即平时 SPI 时钟为低电平
- spi-cpha:这是一个空属性(没有值),表示 CPHA 为 1),即在时钟的第 2 个边沿采样数据
- spi-cs-high:这是一个空属性(没有值),表示片选引脚高电平有效
- spi-3wire:这是一个空属性(没有值),表示使用 SPI 三线模式
- spi-lsb-first:这是一个空属性(没有值),表示使用 SPI 传输数据时先传输最低位(LSB)
- spi-tx-bus-width:表示有几条 MOSI 引脚;没有这个属性时默认只有 1 条 MOSI 引脚
- spi-rx-bus-width:表示有几条 MISO 引脚;没有这个属性时默认只有 1 条 MISO 引脚
- spi-rx-delay-us:单位是毫秒,表示每次读传输后要延时多久
- spi-tx-delay-us:单位是毫秒,表示每次写传输后要延时多久
#
2.3 示例#
3.spidev 的使用(SPI 用户态 API)参考资料:
- 内核驱动:
drivers\spi\spidev.c
- 内核提供的测试程序:
tools\spi\spidev_fdx.c
- 内核文档:
Documentation\spi\spidev
#
3.1 使用方法介绍设备树示例:
设备树里某个 spi 设备节点的 compatible 属性等于下列值,就会跟spidev
驱动匹配:
- “rohm,dh2228fv”
- “lineartechnology,ltc2488”
- “spidev”
匹配之后,spidev.c 的spidev_probe
会被调用,它会:
- 分配一个 spidev_data 结构体,用来记录对于的 spi_device
- spidev_data 会被记录在一个链表里
- 分配一个次设备号,以后可以根据这个次设备号在链表里找到 spidev_data
- device_create:这会生产一个设备节点
/dev/spidevB.D
,B 表示总线号,D 表示它是这个 SPI Master 下第几个设备
以后,我们就可以通过/dev/spidevB.D
来访问 spidev 驱动程序。
#
3.2 驱动程序分析spidev.c 通过 file_operations 向 APP 提供接口:
#
(1)读函数#
(2)写函数#
(3)通过 ioctl 读写参数#
(4)通过 ioclt 读写数据#
3.3 应用程序分析参考tools\spi\spidev_fdx.c
#
(1)显示设备属性#
(2)读数据#
(3)先写再读#
(4)同时读写#
3.4 spidev 的缺点使用 read、write 函数时,只能读、写,这是半双工方式。
使用 ioctl 可以达到全双工的读写。
但是 spidev 有 2 个缺点:
- 不支持中断
- 只支持同步操作,不支持异步操作:就是 read/write/ioctl 这些函数只能执行完毕才可返回
#
3.5 使用 spidev 操作 DAC 模块#
(1)内部框图操作过程为:
- CS 为低
- 在 SCLK 的上升沿,从 DIN 采集 16 位数据,存入上图中的
16-Bit Shift Register
- 在 CS 的上升沿,把
16-Bit Shift Register
中的 10 位数据传入10-Bit DAC Register
,作为模拟量在 OUT 引脚输出
注意:
- 传输的 16 位数据中,高 4 位是无意义的
- 中间 10 位才被转换为模拟量
- 最低 2 位必须是 0
#
(2)时序图使用 SPI 传输的细节:
- SCLK 初始电平为低
- 使用 16 个 SCLK 周期来传输 16 位数据
- 在 SCLK 上升沿读取 DIN 电平
- 在 SCLK 上升沿发出 DOUT 信号
- DOUT 数据来自
16-Bit Shift Register
- 第 1 个数据是上次数据遗留下的 LSB 位
- 其余 15 个数据来自
16-Bit Shift Register
的高 15 位 16-Bit Shift Register
的 LSB 在下一个周期的第 1 个时钟传输- LSB 必定是 0,所以当前的周期里读出
16-Bit Shift Register
的 15 位数据也足够了
#
(3)DAC 公式#
4.SPI 设备驱动程序编写(DAC)#
4.1 编写设备树查看原理图,确定这个设备链接在哪个 SPI 控制器下
在设备树里,找到 SPI 控制器的节点
在这个节点下,创建子节点,用来表示 SPI 设备
示例如下:
#
4.2 注册 spi_driverSPI 设备的设备树节点,会被转换为一个 spi_device 结构体。
我们需要编写一个 spi_driver 来支持它。(of_match_table 中的一项的 compatible 要和设备树中的 compatible 属性对应)
示例如下:
#
4.3 怎么发起 SPI 传输#
4.3.1 接口函数接口函数都在这个内核文件里:include\linux\spi\spi.h
简易函数
复杂的函数
#
5.SPI 设备驱动程序编写(OLED)#
5.1 编写设备树相比于 DAC 模块的 spi 通信,oled 多了 D/C 引脚和 RES 复位引脚。
#
5.2 编写驱动程序:注册 spi_driver在入口函数里面注册:
这样当设备树和 spi_driver 匹配上,就会调用 probe 函数。
在 probe 函数中,注册字符设备,获取 OLED 的复位引脚 RES,数据/命令选择引脚 DC。
#
5.3 编写驱动程序:ioctl、write(使用 SPI 传输函数)在字符设备驱动中,实现 ioctl 和 write,分别负责 OLED 的命令与数据操作。
底层调用核心层调用,spi.h
的 spi_write()
#
6.使用 framebuffer 改造 oled前面的接口并不是标准接口。
软件,如 QT 和 LVGL,它们只关注 fb,而不关心硬件过程,写入一行数据就对应一行像素。
- 分配 Framebuffer;
- 周期性地将 Framebuffer 的数据格式转换为 OLED 显存格式,发送给 OLED 控制器。
#
在 SPI 驱动程序的 probe 函数中添加,注册 fb 程序分配、设置、注册 fb_info 结构体。
分配 fb_info
设置 fb_info
- fb_var
- fb_fix
注册 fb_info
硬件操作
创建内核线程:负责刷新 fb 数据到 OLED 上
kthread_create:创建内核线程,线程处于"停止状态",要运行它需要执行
wake_up_process
kthread_run:创建内核线程,并马上让它处于"运行状态"
kernel_thread
参考 Ubuntu20.04:/home/cheng/work/100askDriver/SPI/07_oled_use_fb