SPI硬件基础
总线拓扑结构
引脚含义
DO(MOSI):Master Output, Slave Input, SPI主控用来发出数据,SPI从设备用来接收数据
DI(MISO) :Master Input, Slave Output, SPI主控用来发出数据,SPI从设备用来接收数据
SCK: Serial Clock,时钟
CS:Chip Select,芯片选择引脚
SPI模式
在SPI协议中,有两个值来确定SPI的模式,分别是:
CPOL(时钟极性)表示SPI CLK的初始电平,0为电平,1为高电平
CPHA (时钟相位)即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿
CPOL CPHA 模式 含义
0 0 0 SPI CLK初始电平为低电平,在第一个时钟沿采样数据
0 1 1 SPI CLK初始电平为低电平,在第二个时钟沿采样数据
1 0 2 SPI CLK初始电平为高电平,在第一个时钟沿采样数据
1 1 3 SPI CLK初始电平为高电平,在第二个时钟沿采样数据
此外部分SPI控制器还可以配置数据流顺序,分别是MSB(高位在前)、LSB(低位在前)
SPI 驱动框架组成
SPI驱动框架包括以下几个部分:
- SPI 核心;管理 SPI 控制器驱动驱动、 SPI 设备驱动、 SPI 设备,此部分由linux提供
- SPI 控制器驱动:用于驱 SOC 上的SPI控制器,此部分由主控芯片厂家提供
- SPI 设备:用于描述 SPI 设备信息和此设备对SPI总线的配置,一般在设备树中编写。
- SPI 设备驱动:用于驱动 SPI 总线上的设备,一移植厂家驱动或自己编写
- spidev :一个通用的SPI设备驱动,提供一种用户空间访问SPI总线的功能(需要设备树支持)
SPI 控制器驱动
对象 struct spi_controller 表示一个 SPI 控制器,其核心成员如下:
//表示继承于struct device,其中的of_node需要指定,否则无法解析设备树
struct device dev;
//总线编号
s16 bus_num;
//片选数量,从设备的片选号不能大于这个数量
u16 num_chipselect;
//控制器所支持的模式
u32 mode_bits;
//最小传输速率
u32 min_speed_hz;
//最大传输速率
u32 max_speed_hz;
//标志,表示控制器的一些特性
u16 flags;
//指示此控制器是否时从设备
bool slave;
//配置SPI控制器
int (*setup)(struct spi_device *spi);
//设置CS的时序
void (*set_cs_timing)(struct spi_device *spi, u8 setup_clk_cycles,
u8 hold_clk_cycles, u8 inactive_clk_cycles);
//SPI数据包传输
int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
int (*transfer_one)(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *transfer);
//维护 queue 的自旋锁
spinlock_t queue_lock;
//管理 SPI 控制器需要传输的 spi_message,一个 spi_message 表示一系列需要传输的数据
struct list_head queue;
//SPI 控制器正在传输的 spi_message
struct spi_message *cur_msg;
//片选引脚编号列表
int *cs_gpios;
//片选引脚描述符列表
struct gpio_desc **cs_gpiods;
//是否使用gpio描述符接口控制控制片选引脚
bool use_gpio_descriptors;
注册、注销SPI控制器驱动
SPI 控制器驱动的核心就是完成对 struct spi_controller 的分配和初始化,然后将其添加到系统中,如下是分配 SPI 控制器并向系统添加和删除 SPI 控制器驱动的函数:
//分配SPI控制器
struct spi_controller *spi_alloc_master(struct device *host, unsigned int size)
//释放SPI控制器
void spi_controller_put(struct spi_controller *ctlr)
#define spi_master_put(_ctlr) spi_controller_put(_ctlr)
//注册SPI控制器
int spi_register_master(struct spi_controller *ctlr)
int spi_register_controller(struct spi_controller *ctlr)
int devm_spi_register_master(struct device *dev, struct spi_controller *ctlr);
int devm_spi_register_controller(struct device *dev, struct spi_controller *ctlr);
//注销SPI控制器
void spi_unregister_master(struct spi_controller *ctlr)
SPI 设备和驱动
SPI 设备用 struct spi_device 表示,它用于描述一个 SPI 设备, SPI 设备的驱动用 struct spi_driver 表示,它用于描述 SPI 设备的驱动。
struct spi_device 的核心成员如下:
//继承的device对象
struct device dev;
//所属控制器
struct spi_controller *controller;
//最大总线频率
u32 max_speed_hz;
//片选号,与控制器的片选列表对应
u8 chip_select;
//总线模式
u32 mode;
//片选引脚,不使用时为-ENOENT
int cs_gpio;
//驱动程序的名称或别名
char modalias[SPI_NAME_SIZE];
//强制匹配字符串
const char *driver_override;
struct spi_driver 的核心成员如下:
//ID匹配表
const struct spi_device_id *id_table;
//设备和驱动匹配成功执行
int (*probe)(struct spi_device *spi);
//设备或驱动卸载执行
int (*remove)(struct spi_device *spi);
//继承的device_driver
struct device_driver driver;
注册/注销 SPI 设备和驱动
可以使用如下函数注册/注销 SPI 设备:
//注册PSI设备
int spi_add_device(struct spi_device *spi);
struct spi_device *spi_new_device(struct spi_controller *, struct spi_board_info *);
//注销SPI设备
void spi_unregister_device(struct spi_device *spi);
可以使用如下函数注册/注销 SPI 设备驱动:
//注册SPI设备驱动
int spi_register_driver(struct spi_driver *sdrv)
//注销SPI设备驱动
void spi_unregister_driver(struct spi_driver *sdrv)
SPI 设备和驱动匹配过程
SPI 注销基于总线设备驱动模型框架,在向 SPI 添加设备或驱动时会执行 SPI 总线的设备去匹配函数,其函数内容如下:
static int spi_match_device(struct device *dev, struct device_driver *drv)
{
const struct spi_device *spi = to_spi_device(dev);
const struct spi_driver *sdrv = to_spi_driver(drv);
//采用driver_override进行强制匹配
if (spi->driver_override)
return strcmp(spi->driver_override, drv->name) == 0;
//设备树匹配
if (of_driver_match_device(dev, drv))
return 1;
//ACPI匹配
if (acpi_driver_match_device(dev, drv))
return 1;
//id_table匹配
if (sdrv->id_table)
return !!spi_match_id(sdrv->id_table, spi);
//采用驱动程序的名称匹配
return strcmp(spi->modalias, drv->name) == 0;
}
SPI 控制器注册过程
构造并初始化一个 struct spi_controller 对象
调用 spi_register_controller 注册 SPI 控制器
使用 spi_controller_check_ops 检查必要参数是否配置正确
通过 idr_alloc 将 SPI 控制器放入到一个 idr 对象中(如果 bus_num 无效会尝试从设备树中获取)
初始化 struct spi_controller 各种资源
从设备树中解析片选引脚,这里根据配置选择gpio编号模式和gpio描述符模式,若选择个屁哦编号模式在调用 spi_register_controller 函数前对相应引脚进行请求操作
使用 device_add 将 struct spi_controller 的 dev 添加到内核
若未提供 transfer 函数,但是提供了 transfer_one 函数或者 transfer_one_message 函数则调用 spi_controller_initialize_queue 函数使能队列参数模式
将 struct spi_controller 对象的 transfer 设置为 spi_queued_transfer
初始化 SPI 队列模式相关资源
将 struct spi_controller 放入 spi_controller_list 链表中
调用 of_register_spi_devices 解析 SPI 设备树
遍历 spi 控制器的设备树子节点,并调用 of_register_spi_device 完成一个子节点的解析
使用 spi_alloc_device 分配一个 SPI 设备
使用 of_modalias_node 读取设备树的 compatible 来配置 SPI 设备的 modalias 参数
使用 of_spi_parse_dt 解析设备树,并根据设备树配置 SPI 设备
使用 spi_add_device 将设备注册到 SPI 总线
检查片选是否合法
调用 spi_dev_set_name ,利用总线名称和片选编号配置设备名称
调用 bus_for_each_dev 遍历 SPI 设备,检查有设备是否存再片选相同问题(通过回调检查)
绑定片选引脚
调用 spi_setup 设置 SPI 设备
调用 device_add 将设备加到系统(如果指定了总线会注册到对应总线,并进行设备驱动匹配,这里应该是 SPI 总线)
SPI 驱动注册过程
构造并初始化 struct spi_driver 对象
调用 spi_register_driver 注册 SPI 驱动
再次配置 struct spi_driver 对象,主要是内部 driver 成员的 bus 、 probe 、 remove 等
调用 driver_register 注册驱动,这里指定的 bus 为 spi_bus_type 所以会注册到 spi_bus_type 总线中,然后执行设备驱动匹配操作
SPI 驱动传输数据
SPI 驱动采用 struct spi_transfer 描述一个数据包,其核心成员如下:
//发送缓存
const void *tx_buf;
//接收缓存
void *rx_buf;
//缓存长度
unsigned len;
//此次传输完成后是否重新获取片选并配置
unsigned cs_change;
//片选无效时间
u16 cs_change_delay;
//cs_change_delay的单位,由us、ns、clk3种选择
u8 cs_change_delay_unit;
//字长
u8 bits_per_word;
//每个字传输完成后延迟多少us
u8 word_delay_usecs;
//每个字传输完成后延迟多少个时钟周期
u16 word_delay;
//在此传输后更改片选状态前之前延迟多少us
u16 delay_usecs;
//时钟速率
u32 speed_hz;
//spi_transfer链表节点
struct list_head transfer_list;
struct spi_transfer 描述的是单个数据包,在 SPI 框架中还需要利用 struct spi_message 将一个或多个数据包组合成一个 message 后才能进行发送, struct spi_message 的核心成员如下:
//要传输的数据包链表
struct list_head transfers;
//所属的 SPI 设备,其中包含了 SPI 设备的各种信息,如SPI控制器、片选、模式、时钟速率等
struct spi_device *spi;
//传输完成回调函数,用于异步传输
void (*complete)(void *context);
//用于再 SPI 控制器中形成一个队列,方便控制器管理需要发送的数据
struct list_head queue;
SPI 传输数据相关的函数如下:
//初始化一个SPI message
void spi_message_init(struct spi_message *m)
//向SPI message中添加一个数据包
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
//启动SPI,进行同步传输
int spi_sync(struct spi_device *spi, struct spi_message *message)
//启动SPI,进行异步传输,传输完成后调用void (*complete)(void *context)函数
int spi_async(struct spi_device *spi, struct spi_message *message)
SPI 传输数据的流程
以 spi_sync 为例, SPI 驱动框架提供了两种 spi_message 传输方案,分别如下:
- 阻塞模式
在注册 SPI 控制器时如果提供了 transfer 函数即工作在阻塞模式。
构造 spi_message (发送过程可能会使用DMA)
调用 spi_sync 进行传输
再 spi_sync 中加锁后直接调用 __spi_sync (这里采用的互斥量加锁)
调用 __spi_validate 检查 spi_message
为 spi_message 绑定传输完成回调函数和 SPI 控制器
检查 spi_controller 的 transfer 是否等于 spi_queued_transfer 函数,若不等于则调用 spi_async_locked 函数
然后在 spi_async_locked 中加锁后调用 __spi_async (这里采用的自旋锁加锁)
通过 spi_controller 控制器中的 transfer 指针调用驱动层的传输函数(应该是一个非阻塞函数,在控制器参数完成后调用 spi_message 传输完成回调函数)
调用 wait_for_completion 等待传输完成
获取传输结果并返回
- 队列模式
在注册 SPI 控制器时不提供 transfer 函数,但提供了 transfer_one 函数,即工作在队列模式。
构造 spi_message ,因为发送过程可能会使用DMA,所以内存最好使用 kmalloc 分配
调用 spi_sync 进行传输
再 spi_sync 中加锁后直接调用 __spi_sync (这里采用的互斥量加锁)
调用 __spi_validate 检查 spi_message
为 spi_message 绑定传输完成回调函数和 SPI 控制器
检查 spi_controller 的 transfer 是否等于 spi_queued_transfer ,等于则利用自旋锁加锁,然后调用 __spi_queued_transfer 将 spi_message 放入 spi_controller 的队列中
调用 __spi_pump_messages 启动传输
通过 spi_controller 的 cur_msg 判断是否正在传输,若正在传输则直接返回
从 spi_controller 中取出的一个 spi_message ,并将其从队列中删除
调用 spi_controller 控制器中的 transfer_one_message 传输一个 spi_message (默认的传输函数是 spi_transfer_one_message )
在 spi_transfer_one_message 函数中多次调用 spi_controller 的 transfer_one 去完成一个 spi_message 的传输(每次调用 transfer_one 后还需要调用 spi_transfer_wait 去等待transfer_one 传输完成)
传输完成后再调用 spi_finalize_current_message
在 spi_finalize_current_message 中启动一个内核线程传输剩余的 spi_message ,然后执行当前 spi_message 的传输完成回调函数
调用 wait_for_completion 函数等待 spi_message 传输完成
获取传输结果并返回
SPI 设备树节点编写
在那条 SPI 总线下挂载设备就在那条总线的设备树节点下添加对应设备的子节点,节点命名规则 [标签:]名称[@地址],节点内容必须包含 reg 属性、 compatible 属性、 spi-max-frequency 属性, reg 属性用于描述片选索引, compatible 属性用于设备和驱动的匹配, spi-max-frequency 用于描述设备可支持的最大 SPI 总线频率,如下是在 SPI1 中添加一个 icm20608 设备节点的示例:
&spi1 {
//描述SPI控制器引脚和片选引脚,并使能I2C控制器
pinctrl-names = "default", "sleep";
pinctrl-0 = <&spi1_pins_a>;
pinctrl-1 = <&spi1_sleep_pins_a>;
cs-gpios = <&gpioz 3 GPIO_ACTIVE_LOW>;
status = "okay";
//描述icm20608设备
spidev: icm20608@0 {
compatible = "alientek,icm20608";
reg = <0>; /* CS #0 */
spi-max-frequency = <8000000>; /* 最大时钟频率 */
spi-cpha; /* cpha=1 */
spi-cpol; /* cpol=1 */
spi-cs-high; /* 片选高电平有效 */
spi-lsb-first; /* 低位在前 */
spi-rx-bus-width = <1>; /* 接收数据线宽度为1B */
spi-tx-bus-width = <1>; /* 发送数据线宽度为1B */
};
};