Linux下SPI设备驱动开发

一.SPI协议介绍

1.硬件连接介绍

        引脚含义:

        DO(MOSI):Master Output, Slave Input,SPI主控用来发出数据,SPI从设备用来接收数据。

        DI(MISO):Master Input, Slave Output,SPI主控用来发出数据,SPI从设备用来接收数据。

        SCK:Serial Clock,时钟。

        CS:Chip Select,芯片选择引脚。

2.SPI协议

a.传输示例

        设现在主控芯片要传输一个0x56数据给SPI FLASH,时序如下:

        首先我们要先拉低 CS0 选中 SPI Flash,0x56的二进制为 0b0101 0110,因此我们在每个 SCK 时钟周期,DO 输出对应的电平。SPI Flash 会在每个时钟周期的上升沿读取 DO 上的电平。 

b.SPI 模式

        在 SPI 协议中,有两个值来确定 SPI 的模式。CPOL:表示 SPICLK 的初始电平,0 为低电平,1 为高电平。CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0 为第一个时钟沿,1 为第二个时钟沿。

        模式0:CPOL=0,CPHA=0,SPICLK初始电平为低电平,在第一个时钟沿采样数据。

        模式1:CPOL=0,CPHA=1,SPICLK初始电平为低电平,在第二个时钟沿采样数据

        模式2:CPOL=1,CPHA=0,SPICLK初始电平为高电平,在第一个时钟沿采样数据

        模式3:CPOL=1,CPHA=1,SPICLK初始电平为低电平,在第一个时钟沿采样数据。

        常用的是模式0和模式3,因为它们都是在上升沿采集数据,不用在乎时钟电平的初始电平是什么,只要在上升沿采集数据就行。 

3.特点

a.采用主-从模式(Master-Slave) 的控制方式

        SPI 规定了两个 SPI 设备之间通信必须由主设备 (Master) 来控制次设备 (Slave). 一个 Master 设备可以通过提供 Clock 以及对 Slave 设备进行片选 (Slave Select) 来控制多个 Slave 设备, SPI 协议还规定 Slave 设备的 Clock 由 Master 设备通过 SCK 管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock, 没有 Clock 则 Slave 设备不能正常工作。

c.采用同步方式(Synchronous)传输数据

        Master 设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样, 来保证数据在两个设备之间是同步传输的。

d.数据交换(Data Exchanges)

        SPI 设备间的数据传输之所以又被称为数据交换, 是因为 SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个 “发送者(Transmitter)” 或者 “接收者(Receiver)”. 在每个 Clock 周期内, SPI 设备都会发送并接收一个 bit 大小的数据, 相当于该设备有一个 bit 大小的数据被交换了。

​         一个 Slave 设备要想能够接收到 Master 发过来的控制信号, 必须在此之前能够被 Master 设备进行访问 (Access)。所以, Master 设备必须首先通过 SS/CS pin 对 Slave 设备进行片选, 把想要访问的 Slave 设备选上。

        ​ 在数据传输的过程中, 每次接收到的数据必须在下一次数据传输之前被采样.。如果之前接收到的数据没有被读取, 那么这些已经接收完成的数据将有可能会被丢弃, 导致 SPI 物理模块最终失效。因此, 在程序中一般都会在 SPI 传输完数据后, 去读取 SPI 设备里的数据, 即使这些数据(Dummy Data)在我们的程序里是无用的。

二.SPI总线设备驱动模型

1.SPI主机驱动

        SPI 主机驱动就是 SOC 的 SPI 控制器驱动,类似 I2C 驱动里面的适配器驱动。 Linux 内核使用 spi_master 表示 SPI 主机驱动, spi_master 是个结构体,定义在 include/linux/spi/spi.h 文件
中,部分代码如下:

struct spi_master {struct device dev;struct list_head list;
.....int (*transfer)(struct spi_device *spi,struct spi_message *mesg);
.....int (*transfer_one_message)(struct spi_master *master,struct spi_message *mesg);
}

        上述代码中,transfer 函数,和 i2c_algorithm 中的 master_xfer 函数一样,控制器数据传输函数。transfer_one_message 函数,也用于 SPI 数据发送,用于发送一个 spi_message,SPI 的数据会打包成 spi_message,然后以队列方式发送出去。

        SPI 主机端最终会通过 transfer 函数与 SPI 设备进行通信,因此对于 SPI 主机控制器的驱
动编写者而言 transfer 函数是需要实现的,因为不同的 SOC 其 SPI 控制器不同,寄存器都不一
样。和 I2C 适配器驱动一样,SPI 主机驱动一般都是 SOC 厂商去编写的。

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

        spi_master的申请与释放:

        spi_alloc_master函数用于申请spi_master,函数定义如下所示:

struct spi_master *spi_alloc_master(struct device *dev,unsigned size)

        dev:设备,一般是 platform_device 中的 dev 成员变量。

        size:私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。

         spi_master 的释放通过 spi_master_put 函数来完成,当我们删除一个 SPI 主机驱动的时候就需要释放掉前面申请的 spi_master,spi_master_put 函数原型如下:

void spi_master_put(struct spi_master *master)

        master:要释放的 spi_master。

        spi_master的注册与注销:

        当 spi_master 初始化完成以后就需要将其注册到 Linux 内核,spi_master 注册函数为
spi_register_master,函数原型如下:

int spi_register_master(struct spi_master *master)

        master:要注册的 spi_master。

        如果要注销 spi_master 的话可以使用 spi_unregister_master 函数,此函数原型为:

void spi_unregister_master(struct spi_master *master)

        master:要注销的 spi_master。

2.SPI设备驱动

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

    struct spi_driver {  int         (*probe)(struct spi_device *spi);  int         (*remove)(struct spi_device *spi);  void            (*shutdown)(struct spi_device *spi);  int         (*suspend)(struct spi_device *spi, pm_message_t mesg);  int         (*resume)(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。

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

void spi_unregister_driver(struct spi_driver *sdrv)

        sdrv:要注销的 spi_driver。

        driver是为device服务的,spi_driver注册时会扫描SPI bus上的设备,进行驱动和设备的绑定,probe函数用于驱动和设备匹配时被调用。SPI的通信是通过消息队列机制,而不是像I2C那样通过与从设备进行对话的方式。

        spi_device结构体的内容如下:

    struct spi_device {  struct device       dev;  struct spi_master   *master;  u32         max_speed_hz;  u8          chip_select;  u8          mode;    u8          bits_per_word;  int         irq;  void            *controller_state;  void            *controller_data;  char            modalias[32];   }; 

        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,
};
/* 驱动入口函数 */
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);

3.SPI 设备和驱动匹配过程

        SPI 设备和驱动的匹配过程是由 SPI 总线来完成的,SPI 总线为 spi_bus_type,定义在 drivers/spi/spi.c 文件中,内容如下所示:

struct bus_type spi_bus_type = {.name = "spi",.dev_groups = spi_dev_groups,.match = spi_match_device,.uevent = spi_uevent,
};

        SPI 设备和驱动的匹配函数为 spi_match_device,其函数内容如下:

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);/* 用于完成设备树设备和驱动匹配 */if (of_driver_match_device(dev, drv))return 1;/* 用于ACPI形式的匹配 */if (acpi_driver_match_device(dev, drv))return 1;/* 用于传统无设备树设备和驱动匹配 */if (sdrv->id_table)return !!spi_match_id(sdrv->id_table, spi);/* 比较modalias成员变量和name成员变量是否相等 */return strcmp(spi->modalias, drv->name) == 0;
}

4.SPI 设备数据收发处理

        当向 Linux 内核注册成功 spi_driver 后就可以使用 SPI 核心层提供的 API 函数来对设备进行读写操作了。首先是 spi_transfer 结构体,此结构体用于描述 SPI 传输信息,结构体内容如下:

struct spi_transfer {const void *tx_buf;	//保存着要发送的数据void *rx_buf;		//用于保存接收到的数据unsigned len;		//要进行传输的数据长度dma_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;
};

        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;	struct list_head queue;void *state;
};

        在使用spi_message之前需要对其进行初始化:

void spi_message_init(struct spi_message *m)
//m:要初始化的 spi_message

        spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中:

void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
//t:要添加到队列中的 spi_transfer
//m:spi_transfer 要加入的 spi_message

        spi_message 准备好后既可以进行数据传输,数据传输分为同步传输和异步传输:

/***同步传输会阻塞的等待 SPI 数据传输完成***/
int spi_sync(struct spi_device *spi, struct spi_message *message)
//spi:要进行数据传输的 spi_device
//message:要传输的 spi_message
/***异步传输不会阻塞等待,需设置spi_message中的
complete回调函数,当异步传输完成后此函数就会被调用***/
int spi_async(struct spi_device *spi, struct spi_message *message)
//spi:要进行数据传输的 spi_device
//message:要传输的 spi_message

        SPI 数据传输示例代码如下:

/********** SPI 多字节发送 **********/
static int spi_send(struct spi_device *spi, u8 *buf, int len){int ret;struct spi_message m;struct spi_transfer t = {	  //1. 定义一个spi_transfer结构体变量,并设置成员变量.tx_buf = buf,.len = len,};spi_message_init(&m); 		  //2. 初始化 spi_messagespi_message_add_tail(t, &m);  //3. 将 spi_transfer 添加到 spi_message 队列ret = spi_sync(spi, &m); 	  //4. 同步传输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 = {	  //1. 定义一个spi_transfer结构体变量,并设置成员变量.rx_buf = buf,.len = len,};spi_message_init(&m); 		  //2. 初始化 spi_messagespi_message_add_tail(t, &m);  //3. 将 spi_transfer 添加到 spi_message 队列ret = spi_sync(spi, &m);	  //4. 同步传输return ret;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/92219.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/92219.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用Dify构建气象智能体:从0到1搭建AI工作流实战指南

作为一名Agent产品经理,我最近在负责气象智能体的建设项目。传统气象服务面临三大痛点:数据孤岛严重(气象局API、卫星云图、地面观测站等多源数据格式不一)、响应链路长(从数据采集到预警发布需人工介入多个环节)、交互体验单一(用户只能被动接收标准化预警,无法个性化…

Android NDK ffmpeg 音视频开发实战

文章目录接入FFmpeg1.下载FFmpeg 源码2.编译FFmpeg.so库异常处理3.自定义FFmpeg交互so库创建4.配置CMakeLists.txt5.CMakeLists.txt 环境配置6.Native与Java层调用解码器准备接入FFmpeg 1.下载FFmpeg 源码 FFmpeg官网地址 2.编译FFmpeg.so库 移动 FFmpeg 源码文件夹至 Andr…

使用 go-redis-entraid 实现 Entra ID 无密钥认证

1、依赖与安装 步骤命令说明安装(或升级) go-redis v9.9go get github.com/redis/go-redis/v9latestentraid 必须 ≥ 9.9.0安装 go-redis-entraidgo get github.com/redis/go-redis-entraid自动拉取 transit 依赖 2、认证方式一览 方式说明创建 Stream…

window上docker安装RabbitMQ

1、要进http://localhost:15672管理页面需要安装management版本2、搜索镜像并pull3、启动镜像时将端口映射出来4、启动成功,点击可查看日志详情,浏览器访问5、直接使用guest/guest登录会报错User can only log in via localhost解决办法有两个&#xff1…

异世界历险之数据结构世界(排序(插入,希尔,堆排))

前言 介绍 插入排序 基本知识: 直接插入排序是一种简单的插入排序法,其基本思想是: 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 直接插入…

oracle 数据库中,将几张表的数据按指定日期范围实时同步至同一个数据库的备份表中。

以下是一个Oracle数据库中实现表数据按指定日期范围实时同步至备份表的解决方案。这个方案使用存储过程和触发器组合实现: 1. 创建备份表结构 首先需要为每张需要备份的表创建对应的备份表,结构与原表相同: -- 为原表创建备份表(示…

电脑网络连接正常,微信、QQ能正常使用,但无法访问网页?DNS问题的解决方案和背后原理。

文章目录1. 问题背景2. 解决方案2.1 手动刷新DNS2.1.1 Windows版本2.1.2 Mac版本2.2 手动设置DNS服务器2.2.1 Windows版2.2.2 Mac版2.3 其他解决方案3. DNS是什么?3.1 详细解释DNS3.1.1 A distributed, hierarchical database(一个分布式和分层数据库结构…

【HTML】图片比例和外部div比例不一致,最大程度占满

图片比例和外部div比例不一致&#xff0c;最大程度占满&#xff0c;并且图片比例不变 其中1.jpg,2.jpg,1.html在同一目录 |-----|- 1.jpg|- 2.jpg|- 1.html1.jpg2.jpg<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /&g…

如何使用python网络爬虫批量获取公共资源数据技术

如何快速批量地获取海量公共资源数据决定了科研的效率。Python网络爬虫是快速批量获取网络数据的重要手段&#xff0c;它按照发送请求、获得页面、解析页面、下载内容、储存内容等流程&#xff1f; 一&#xff1a;Python软件的安装及入门1 Python软件安装及入门1)Anaconda软件安…

Kiro vs Cursor: AI IDE 终极对比指南

概述 随着生成式 AI 革命性地改变了我们编写代码的方式&#xff0c;新一代 AI 驱动的集成开发环境 (IDE) 正在崛起。Kiro 和 Cursor 代表了这一运动的前沿&#xff0c;但它们采用了截然不同的方法。 核心理念对比 特性AWS KiroCursor核心理念结构化开发流程 (Spec-driven)对…

Python获取网页乱码问题终极解决方案 | Python爬虫编码处理指南

在Python网络爬虫开发中&#xff0c;乱码是最常见的问题之一。本文将深入探讨乱码产生的原因&#xff0c;并提供多种有效的解决方案&#xff0c;帮助您彻底解决Python获取网页内容时的乱码问题。常见网页编码格式编码类型使用场景Python解码方式UTF-8现代网站标准编码.decode(u…

Android MTK平台预置多张静态壁纸

执行 adb shell pm list package -f wallpaper 命令&#xff0c;查看壁纸应用路径&#xff1a; /product/app/MtkWallpaperPicker/MtkWallpaperPicker.apkcom.android.wallpaperpicker 结果中带 Mtk 就可确定MTK有对应用进行重构。其源码路径在 vendor/mediatek/proprietary/…

基于Django的个人博客系统开发(开题报告)

毕业论文(设计)开题报告论文(设计)题目 基于Django的个人博客系统开发 1.选题目的和意义 随着云服务器的普及化以及编程培训机构大量涌现,学习网站开发技术以及编程技术,通过租用个人云服务器部署代码,构建个人博客网站,创建学习文档,记录学习过程,与他人交流技术学…

C++ 分配内存释放内存

C 分配内存释放内存一、new、delete、malloc和free最简单的分配内存自定义对象分配和释放内存二、new、delete与虚析构的问题三、一维、二维、多维数值创建和释放一维二维多维四、new的缺点以及连续内存的优点一、new、delete、malloc和free 最简单的分配内存 int* p_m (int*…

奥比中光深度相机开发

一、开发环境准备 1.1 硬件要求 奥比中光深度相机&#xff08;如Astra Pro、Gemini等&#xff09;USB 3.0接口&#xff08;确保数据传输稳定&#xff09;支持OpenGL的显卡&#xff08;可选&#xff0c;用于点云可视化&#xff09; 1.2 软件环境 SDK安装&#xff1a; 从奥比…

标题 “Python 网络爬虫 —— selenium库驱动浏览器

一、Selenium 库核心认知 Selenium 库是 Web 应用程序测试与自动化操作的利器 &#xff0c;能驱动浏览器&#xff08;如 Edge、Firefox 等&#xff09;执行点击、输入、打开、验证等操作 。与 Requests 库差异显著&#xff1a;Requests 库仅能获取网页原始代码&#xff0c;而 …

从实践出发--探究C/C++空类的大小,真的是1吗?

文章目录测试代码VS2022正常运行编译失败GCC总结Author: NemaleSu Data: 2025/07/21 测试环境&#xff1a; Win11&#xff1a;VS2022Ubuntu22.04&#xff1a;gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 相信众多cpper听过太多书籍、视频、文档、博客等资料&#xff0c;说C/C…

数据结构自学Day11-- 排序算法

一、排序算法的概念排序&#xff08;Sorting&#xff09;是指&#xff1a;将一组“无序”的数据&#xff0c;按照某种“顺序规则”排列成“有序”的过程。1、按排序顺序分类&#xff1a;升序&#xff1a;从小到大排列&#xff0c;如 1, 3, 5, 7, 9降序&#xff1a;从大到小排列…

电子元器件—三极管(一篇文章搞懂电路中的三极管)(笔记)(面试考试必备知识点)

三极管的定义及工作原理1. 定义三极管&#xff08;Transistor&#xff09;是一种具有三层半导体材料&#xff08;P-N-P 或 N-P-N&#xff09;构成的半导体器件&#xff0c;用于信号放大、开关控制和信号调制等应用。三极管有三个引脚&#xff1a;发射极&#xff08;Emitter&…

数据结构之克鲁斯卡尔算法

前言&#xff1a;和Prim算法一样&#xff0c;Kruskal 算法也是用来生成最小生成树的&#xff0c;这篇文章来学习一下Kruskal算法的实现 一、实现流程 初始化的时候&#xff0c;将所有的边用一个数组存储&#xff0c;并且按权值从小到大进行排序&#xff0c;每次选一个权值最小的…