SPI通信及设备驱动

ops/2025/2/10 21:01:08/

3.SPI通信-重要

参考博客:SPI原理超详细讲解---值得一看-CSDN博客

SPI(Serial Peripheral interface)**串行外围设备接口**

SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。

SPI可以无中断传输数据,可以连续地发送或接收任意数量的位。但是I2C和UART中,数据以数据包的形式发送,有限定位数。 在SPI设备中,设备分为主机控制设备(通常是微控制器)和从机(通常是传感器,显示器和存储芯片)设备,从机从主机那获取指令。

3.1SPI主从模式

SPI分为主、从两种模式,一个SPI通讯系统需要包含一个(且只能是一个)主设备,一个或多个从设备。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起。当存在多个从设备时,通过各自的片选信号进行管理。

SPI是全双工且SPI没有定义速度限制,一般的实现通常能达到甚至超过10 Mbps

3.2SPI信号线

SPI接口一般使用四条信号线通信: SDI(数据输入),SDO(数据输出),SCK(时钟),CS(片选)

MISO: 主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。 MOSI: 主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。 SCLK:串行时钟信号,由主设备产生。 CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为“片选引脚”,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。

硬件上为4根线

SPI一对一

在这里插入图片描述

SPI一对多

在这里插入图片描述

3.3时钟信号

每个时钟周期传输一位数据,因此数据传输的速度取决于时钟信号的频率。 时钟信号由于是主机配置生成的,因此SPI通信始终由主机启动。 设备共享时钟信号的任何通信协议都称为同步。SPI是一种同步通信协议,还有一些异步通信不使用时钟信号。 例如在UART通信中,双方都设置为预先配置的波特率,该波特率决定了数据传输的速度和时序。

3.4片选信号

主机通过拉低从机的CS/SS来使能通信。主机可以与存在多个CS/SS引脚,允许主机与多个从机进行通信。

3.5传输步骤

1.主机输出时钟信号 在这里插入图片描述

2.主机拉低SS/CS引脚,激活从机 在这里插入图片描述

3.主机通过MOSI将数据发送给从机 在这里插入图片描述

4.如果需要相应,则从机通过MISO将数据返回给从机 在这里插入图片描述

3.6.IMX6ULL-SPI驱动和设备

参考博客:【IMX6ULL学习笔记】二十一、SPI驱动和设备 - 酷电玩家 - 博客园

i.MX6ULL驱动开发 | 13 - Linux SPI 驱动框架_MCUlover666的技术博客_51CTO博客

一.LINUX下SPI驱动框架简介
1.SPI主机驱动

SPI 主机驱动就是 SOC (System on Chip,系统级芯片或片上系统)的 SPI 控制器驱动,类似 I2C 驱动里面的适配器驱动。Linux 内核使用 spi_master 表示 SPI 主机驱动,spi_master 是个结构体,定义在include/linux/spi/spi.h 文件中,内容如下(有缩减):

struct spi_master {struct device dev;
​struct list_head list;
......s16 bus_num;
​/* chipselects will be integral to many controllers; some others* might use board-specific GPIOs.*/u16 num_chipselect;
​/* some SPI controllers pose alignment requirements on DMAable* buffers; let protocol drivers know about these requirements.*/u16 dma_alignment;
​/* spi_device.mode flags understood by this controller driver */u16 mode_bits;
​/* bitmask of supported bits_per_word for transfers */u32 bits_per_word_mask;
....../* limits on transfer speed */u32 min_speed_hz;u32 max_speed_hz;
​/* other constraints relevant to this driver */u16 flags;
....../* lock and mutex for SPI bus locking */spinlock_t bus_lock_spinlock;struct mutex bus_lock_mutex;
​/* flag indicating that the SPI bus is locked for exclusive use */bool bus_lock_flag;
......int (*setup)(struct spi_device *spi);
​
......
41    int (*transfer)(struct spi_device *spi,struct spi_message *mesg);
......
​
45    int (*transfer_one_message)(struct spi_master *master,struct spi_message *mesg);
......
};

第 41 行:transfer 函数,和 i2c_algorithm 中的 master_xfer 函数一样,控制器数据传输函数。 第 45 行:transfer_one_message 函数,也用于 SPI 数据发送,用于发送一个 spi_message,SPI 的数据会打包成 spi_message,然后以队列方式发送出去。 也就是 SPI 主机端最终会通过 transfer 函数与 SPI 设备进行通信,因此对于 SPI 主机控制器的驱动编写者而言 transfer 函数是需要实现的,因为不同的 SOC 其 SPI 控制器不同,寄存器都不一样。

和 I2C 适配器驱动一样,SPI 主机驱动一般都是 SOC 厂商去编写的,SOC 的使用者不用操心。

SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册 spi_master。

2.SPI设备驱动

spi 设备驱动也和 i2c 设备驱动也很类似,Linux 内核使用 spi_driver 结构体来表示 spi 设备驱动,在编写 SPI 设备驱动的时候需要实现 spi_driver 。spi_driver 结构体定义在include/linux/spi/spi.h 文件中,内容如下:

struct spi_driver {const struct spi_device_id *id_table;int (*probe)(struct spi_device *spi);int (*remove)(struct spi_device *spi);void (*shutdown)(struct spi_device *spi);struct device_driver driver;
};

可以看出,spi_driver 和 i2c_driver、platform_driver 基本一样,当 SPI 设备和驱动匹配成功以后 probe 函数就会执行。

注册驱动: 同样的,spi_driver 初始化完成以后需要向 Linux 内核注册,spi_driver 注册函数为 spi_register_driver,函数原型如下:

int spi_register_driver(struct spi_driver *sdrv)

函数参数和返回值含义如下:

sdrv:要注册的 spi_driver。
返回值:0,注册成功;赋值,注册失败。

注销驱动: 注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函数完成 spi_driver 的注销,函数原型如下:

void spi_unregister_driver(struct spi_driver *sdrv)

函数参数和返回值含义如下:

sdrv:要注销的 spi_driver。
返回值:无。

spi_driver 注册示例程序如下:

/* probe 函数 */
static int xxx_probe(struct spi_device *spi)
{/* 具体函数内容 */return 0;
}
​
/* remove 函数 */
static int xxx_remove(struct spi_device *spi)
{/* 具体函数内容 */return 0;
}
​
/* 传统匹配方式 ID 列表 */
static const struct spi_device_id xxx_id[] = {{"xxx", 0},{}
};
​
/* 设备树匹配列表 */
static const struct of_device_id xxx_of_match[] = {{ .compatible = "xxx" },{ /* Sentinel */ }
};
​
/* SPI 驱动结构体 */
static struct spi_driver xxx_driver = {.probe = xxx_probe,.remove = xxx_remove,.driver = {.owner = THIS_MODULE,.name = "xxx",.of_match_table = xxx_of_match,},.id_table = xxx_id,
};1-37
​
/* 驱动入口函数 */
static int __init xxx_init(void)
{return spi_register_driver(&xxx_driver);
}
​
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{spi_unregister_driver(&xxx_driver);
}
​
module_init(xxx_init);
module_exit(xxx_exit);

第 1~37 行:spi_driver 结构体,需要 SPI 设备驱动人员编写,包括匹配表、probe 函数等。 和 i2c_driver、platform_driver 一样,就不详细讲解了。 第 40~43 行:在驱动入口函数中调用 spi_register_driver 来注册spi_driver。 第 46~49 行:在驱动出口函数中调用 spi_unregister_driver 来注销 spi_driver。

3.SPI设备驱动编写流程
1)SPI设备信息描述

①、IO 的 pinctrl 子节点创建与修改 首先是根据所使用的 IO 来创建或修改 pinctrl 子节点,要注意的就是检查相应的 IO 有没有被其他的设备所使用,如果有的话需要将其删除掉!

②、SPI 设备节点的创建与修改 采用设备树的情况下,SPI 设备信息描述就通过创建相应的设备子节点来完成,打开 imx6qdl-sabresd.dtsi 这个设备树头文件,在此文件里面找到如下所示内容:

&ecspi1 {
2    fsl,spi-num-chipselects = <1>;
3   cs-gpios = <&gpio4 9 0>;
4    pinctrl-names = "default";
5    pinctrl-0 = <&pinctrl_ecspi1>;
6    status = "okay";
7
8    flash: m25p80@0 {
9        #address-cells = <1>;
10        #size-cells = <1>;
11        compatible = "st,m25p32";
12        spi-max-frequency = <20000000>;
13        reg = <0>;
14    };
15};

示例代码是 I.MX6Q 的一款板子上的一个 SPI 设备节点,在这个板子的 ECSPI 接 口上接了一个 m25p80,这是一个 SPI 接口的设备。 第 2 行:设置“fsl,spi-num-chipselects”属性为 1,表示只有一个设备。 第 3 行:设置“cs-gpios”属性,也就是片选信号为 GPIO4_IO09。 第 4 行:设置“pinctrl-names”属性,也就是 SPI 设备所使用的 IO 名字。 第 5 行:设置“pinctrl-0”属性,也就是所使用的 IO 对应的 pinctrl 节点。 第 6 行:将 ecspi1 节点的“status”属性改为“okay”。 第 8~15 行:ecspi1 下的 m25p80 设备信息,每一个 SPI 设备都采用一个子节点来描述其设备信息。第 8 行的“m25p80@0”后面的“0”表示 m25p80 接到了 ECSPI 的通道 0 上。这个要根据自己的具体硬件来设置。第 11 行是设备的 compatible 属性值,用于匹配设备驱动。第 12 行“spi-max-frequency”属性设置 SPI 控制器的最高频率,这个要根据所使用的 SPI 设备来设置,比如在这里将 SPI 控制器最高频率设置为 20MHz。第 13 行 reg 属性设置 m25p80 这个设备所使用的 ECSPI 通道,和“m25p80@0”后面的“0”一样。

2)SPI设备数据收发处理流程

SPI 设备驱动的核心是 spi_driver。当向 Linux 内核注册成功 spi_driver 以后就可以使用 SPI 核心层提供的 API 函数来对设备进行读写操作了。

首先是 spi_transfer 结构体,此结构体用于描述 SPI 传输信息,内容如下:

struct spi_transfer {/* it's ok if tx_buf == rx_buf (right?)* for MicroWire, one buffer must be null* buffers must work with dma_*map_single() calls, unless* spi_message.is_dma_mapped reports a pre-existing mapping*/
7    const void *tx_buf;
8    void *rx_buf;
9    unsigned len;
10dma_addr_t tx_dma;dma_addr_t rx_dma;struct sg_table tx_sg;struct sg_table rx_sg;
​unsigned cs_change:1;unsigned tx_nbits:3;unsigned rx_nbits:3;#define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */#define SPI_NBITS_DUAL 0x02 /* 2bits transfer */#define SPI_NBITS_QUAD 0x04 /* 4bits transfer */u8 bits_per_word;u16 delay_usecs;u32 speed_hz;
​struct list_head transfer_list;
};

第 7 行:tx_buf 保存着要发送的数据。 第 8 行:rx_buf 用于保存接收到的数据。 第 9 行:len 是要进行传输的数据长度,SPI 是全双工通信,在一次通信中发送和接收的字节数都是一样的,所以 spi_transfer 中也就没有发送长度和接收长度之分。

spi_transfer 需要组织成 spi_message,spi_message 也是一个结构体,内容如下:

struct spi_message {struct list_head transfers;
​struct spi_device *spi;
​unsigned is_dma_mapped:1;
....../* completion is reported through a callback */void (*complete)(void *context);void *context;unsigned frame_length;unsigned actual_length;int status;
​/* for optional use by whatever driver currently owns the* spi_message ... between calls to spi_async and then later* complete(), that's the spi_master controller driver.*/struct list_head queue;void *state;
};

在使用 spi_message 之前需要对其进行初始化,spi_message 初始化函数为spi_message_init,函数原型如下:

void spi_message_init(struct spi_message *m)

函数参数和返回值含义如下:

m:要初始化的 spi_message。
返回值:无。

spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中,这里我们要用到 spi_message_add_tail 函数,此函数原型如下:

void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)

函数参数和返回值含义如下:

t:要添加到队列中的 spi_transfer。
m:spi_transfer 要加入的 spi_message。
返回值:无。

spi_message 准备好以后就可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:

int spi_sync(struct spi_device *spi, struct spi_message *message)

函数参数和返回值含义如下:

spi:要进行数据传输的 spi_device。
message:要传输的 spi_message。
返回值:无。

异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete 成员变量,complete 是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。SPI 异步传输函数为 spi_async,函数原型如下:

int spi_async(struct spi_device *spi, struct spi_message *message)

函数参数和返回值含义如下:

spi:要进行数据传输的 spi_device。
message:要传输的 spi_message。
返回值:无。

本次实验中,采用同步传输方式来完成 SPI 数据的传输工作,也就是 spi_sync 函数。综上所述,SPI 数据传输步骤如下: ①、申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量,tx_buf 为要发送的数据。然后设置 rx_buf 成员变量,rx_buf 保存着接收到的数据。最后设置 len 成员变量,也就是要进行数据通信的长度。 ②、使用 spi_message_init 函数初始化 spi_message。 ③、使用spi_message_add_tail函数将前面设置好的spi_transfer添加到spi_message队列中。 ④、使用 spi_sync 函数完成 SPI 数据同步传输。

通过 SPI 进行 n 个字节的数据发送和接收的示例代码如下所示:

/* SPI 多字节发送 */
static int spi_send(struct spi_device *spi, u8 *buf, int len)
{int ret;struct spi_message m;struct spi_transfer t = {.tx_buf = buf,.len = len,};spi_message_init(&m); /* 初始化 spi_message */spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */ret = spi_sync(spi, &m); /* 同步传输 */return ret;
}
​
/* SPI 多字节接收 */
static int spi_receive(struct spi_device *spi, u8 *buf, int len)
{int ret;struct spi_message m;struct spi_transfer t = {.rx_buf = buf,.len = len,};spi_message_init(&m); /* 初始化 spi_message */spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */ret = spi_sync(spi, &m); /* 同步传输 */return ret;
}

3.7SPI优缺点

优点:无起始位和停止位,因此数据可以持续传输不会中断;数据传输速率快(比I2C快几乎两倍)。独立的MISO、MOSI可以同时发送和接收数据。 缺点:使用四根线(I2C使用两根线),没有信号接收成功的确认(I2C由此功能),没有任何形式的错误检查(UART中的奇偶校验位)。


http://www.ppmy.cn/ops/157357.html

相关文章

配置 VS Code 调试 ROS Python 脚本:完整步骤

在 Ubuntu 系统上使用 ROS 和 VS Code 进行 Python 开发时&#xff0c;可能会遇到一些环境配置的问题&#xff0c;特别是当需要加载 ROS 环境变量以及确保正确使用 Python 3 环境时。以下是如何配置 launch.json 和 tasks.json 来确保 VS Code 调试环境能够正确加载 ROS 和 Pyt…

webpack系统学习

webpack4和webpack5区别1---loader_webpack4与webpack5处理图片的不同-CSDN博客 webpack4和webpack5区别2---代码压缩_webpack4如何使用terser-CSDN博客 webpack4和webpack5区别3---缓存_cacheprune-CSDN博客 webpack4和webpack5区别4---自动清除打包目录_webpack4打包目录清…

git命令行删除远程分支、删除远程提交日志

目录 1、从本地通过命令行删除远程git分支2、删除已 commit 并 push 的记录 1、从本地通过命令行删除远程git分支 git push origin --delete feature/feature_xxx 删除远程分支 feature/feature_xxx 2、删除已 commit 并 push 的记录 git reset --hard 7b5d01xxxxxxxxxx 恢复到…

【数据结构 C 语言实现】栈和队列

目录 栈和队列1 栈1.1 栈的结构体定义1.2 基本功能实现1.2.1 创建栈1.2.2 销毁栈1.2.3 入栈1.2.4 出栈1.2.5 判断栈是否为空1.2.6 获取栈顶元素&#xff08;不弹出&#xff09;1.2.7 获取栈的当前大小 1.3 代码实现 2 队列2.1 循环队列的结构体定义2.2 基本功能实现2.2.1 创建循…

视觉硬件选型和算法选择(CNN)

基础知识 什么是机械视觉: 机械视觉是一种利用机器代替人眼来进行测量和判断的技术&#xff0c;通过光学系统、图像传感器等设备获取图像&#xff0c;并运用图像处理和分析算法来提取信息&#xff0c;以实现对目标物体的识别、检测、测量和定位等功能。 机械视觉与人类视觉有什…

idea插件开发dom4j报错:SAXParser cannot be cast to class org.xml.sax.XMLReader

手打不易&#xff0c;如果转摘&#xff0c;请注明出处&#xff01; 注明原文&#xff1a;https://blog.csdn.net/q258523454/article/details/145512328 dom4j报错 idea插件使用到了dom4j依赖&#xff0c;但是报错&#xff1a; I will print the stack trace then carry on…

软件测试之单元测试

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、什么是单元测试&#xff1f; 单元测试是指&#xff0c;对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作&#xff0c;这里的最…

Vue全流程--Vue2路由

引入路由的原因&#xff1a; 实现单页面应用&#xff08;SPA&#xff09; 什么是单页面应用&#xff1a; 1、点击跳转链接后直接在原本的页面展示。路径发生相应改变 2、整个应用只有一个完整页面 3、数据需要通过ajax获取 Vue2中的路由是什么&#xff1a; Vue2路由是一…