当前位置: 首页 > news >正文

Linux驱动学习(SPI驱动)

一、SPI硬件框架和协议内容

每个 SPI 主控接口通常包含三条信号线:时钟线(SCK)、主输出从输入线(MOSI/DO(Master Out Slave In))和主输入从输出线(MISO/DI),此外还有一个片选引脚(CS)。为了保证同一时刻仅能访问一个从设备,片选引脚在任意时间只能有一个被激活。

要和从设备进行通信,首先应该发出片选信号,在每个时钟周期传输一位数据。

其中,我们要注意两个问题:SCK的初始电平(CPOL极性)和在SCK的第1/2个边沿传输数据(CPHA相位)?下面是SPI的模式选择(无论哪种模式,我们都可以同时接收和发出数据):

CPOL = 0 的时候表示SCK时钟线平时是0;为1表示平时是1;

CPHA = 0 的时候表示在SCK第一个边沿传输数据,为1 表示在SCK第二个边沿传输数据。

二、SPI控制器

三、 SPI总线驱动模型

3.1 SPI总线设备树

3.2 注册流程

在Linux系统中,将SPI控制器抽象成一个spi_master结构体。

按照总线驱动模型的工作流程,首先由设备树中 SPI 总线节点的 compatible 属性与驱动程序中定义的 compatible 属性进行匹配,匹配成功后会调用驱动程序中的 probe 函数。在 probe 函数中,会创建并初始化一个 spi_master 结构体,同时解析其下挂的 SPI 设备在设备树中的配置信息。随后,通过 spi_bus_type 中的 match 机制,将 SPI 设备与相应的驱动(spi_dev_drv)进行匹配和绑定。

以RK3568为例,找到对应驱动程序为spi_rockchip.c,当设备树中的spi总线compatible属性和id_table匹配,会调用改函数的probe函数,RK系列将spi_master封装成spi_controller结构体,调用到devm_spi_register_controller()-->spi_register_controller()-->of_spi_register_master()
去注册结构体并且解析设备树得到相关节点的片选引脚。

of_spi_register_master代码

在spi_register_controller()中注册完驱动之后继续注册了spi_device,of_register_spi_devices()。

四、spidev的使用(SPI用户态API)

对于简单的spi设备,不需要去写spi相关的设备驱动文件。直接使用应用程序就可以通过spidev去操作设备了。

弊端:如果是我们的spi设备需要涉及到中断相关的信息就不能使用这个万能的驱动程序了。

4.1 spidev.c 驱动程序分析 /drivers/spi/spidev.c

spidev.c这个驱动程序比较好理解,架构和字符设备驱动一样,首先是在init函数中register_chrdev和class_create,接着不是device_create而是使用spi_register_driver,注册这个驱动程序,当内核中设备树某一个节点的compatible属性和驱动程序提供的compatible属性一样,就会调用到驱动程序的probe函数。

在probe函数中会去构造一个spidev_data结构体,分配设置这个结构体,找到主设备下的为空的次设备号分配个这个device,然后再去device_create创建节点。

添加完节点之后会将这个设备挂入spidev的设备节点下。

等到当有应用函数调用open设备节点的时候,会进入到该驱动程序的open函数,在open函数中,遍历spi_dev设备链表,通过次设备号,找到挂在这个链表中的spidev设备,并且将这个设备设置成file的私有成员变量,下次调用read/write的时候直接可以从file中取出该spi设备。

我们再来看一下read/write函数。写函数也和读函数是一样的。

在这个驱动中,我们单独调用write/read函数是无法进行同时读写操作的,我们必须要调用该驱动的ioctl函数。

实例的应用程序如下(同时读写的长度是一样的):

4.2 如果要使用这个驱动程序,必须要满足的条件

五、SPI应用实例

5.1 原理图

DAC芯片如图所示,移位寄存器中只有中间10位有效数据会将数据上传到DAC寄存器。

看一下时序图,在时钟引脚的上升沿移入一位一位的数据到移位寄存器,在片选引脚的上升沿把其中的10位数据放入DAC寄存器。

这里在应用编程要注意的一点是:当SCLK第一个上升沿的时候,DOUT输出的第一位是由上次16位数据的最低位(必定是0),而不是当前传输的最高位。在传输最后的时候DOUT的输出是16位中的前15位。

5.2 使用spidev.c驱动程序的应用程序编写

5.2.1 将spidev这个驱动程序编译进内核

5.2.2 dts配置,找到一个spi_master,在其中添加一个子节点

根据dac芯片手册,可以看到时钟引脚的低电平最低25ns,高电平25ns.

合起来就是50ns,可以得出f = 1 / 10 ^ -6。

5.2.3  编写应用程序

要做的事情是:写入16位数据同时读出16位数据。(只能使用ioctl) LSB 00 MSB0000

// SPDX-License-Identifier: GPL-2.0
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>#include <linux/types.h>
#include <linux/spi/spidev.h>/* ./dac_test /dev/spidevB.D val*/
int main(int argc, char **argv)
{   int fd;struct spi_ioc_transfer	xfer[1];unsigned char buf[2];unsigned int val;int	status;unsigned char rx_buf[2];unsigned char tx_buf[2];if(argc != 3){printf("Usage: %s [/dev/spidevB.D] [val]\n" , argv[0]);return -1;}fd = open(argv[1] , O_RDWR);if (fd < 0) {printf("can not open file\n");return -1;}val = strtoul(argv[2], NULL, 0);val <<= 2; /* bit0,bit1 = 0, 强制让最低两位变成 0 */val &= 0xffc; /* 只保留10bit *//* 先传输tx_buf[0] 后传输tx_buf[1] , 所以tx_buf[0]存入高字节数据 */tx_buf[1] = val & 0xff;tx_buf[0] = (val >> 8) & 0xff;memset(xfer, 0, sizeof xfer);xfer[0].rx_buf = rx_buf;xfer[0].tx_buf = tx_buf;xfer[0].len = 2; status = ioctl(fd, SPI_IOC_MESSAGE(1), xfer);val = (rx_buf[0] << 8) | (rx_buf[1]);val >>= 2;printf("Pre val = %d\n", val);return 0;
}

这里由于SPI 总线 一次只能传 8 位(一个字节),但是 DAC 芯片需要传一个 10 位或更多位的数值。所以你必须把这个多位整数拆成 高字节低字节 两部分,分别通过 SPI 发出去。

那么在这段代码中为什么我们要val <<= 2呢?原因是: DAC 芯片的 SPI 协议格式 上。低两位必须是0;

tx_buf[1] = val & 0xff;
tx_buf[0] = (val >> 8) & 0xff;

这两行的代码主要是因为,SPI先传高字节,再传低字节,所以先传的tx_buf[0]应该是高字节的内容,tx_buf[1]则是低字节的内容。

应用程序代码关键在于,在ioctl之前,如何去构造传输函数,拿到rxbuf之后如何去解析数据。

六、编写spi设备驱动程序

6.1 理论

重点是:如何发起SPI传输,构造read\write\ioctl函数?

特性:对于SPI传输,只要发送n个byte就会收到n个byte。

如果不在乎某个输入或者输出,可以将其的buf置为NULL。

如果有想进行多次传输,需要用链表将多个transfer结构题连接在一起,这时候需要一个链表头spi_message。

所以说构造流程如下:分配设置一个spi_message,将spi_message结构体挂在该msg下,使用spidev_sync函数发起spi传输。

6.2 spi传输实例:

6.3 spi_dev驱动程序代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/list.h>
#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/compat.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/acpi.h>#include <linux/spi/spi.h>
#include <linux/spi/spidev.h>#include <linux/uaccess.h>static int major;
static struct spi_device *dac;
static struct class * dac_class;
static long spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{int val;int err;unsigned char rx_buf[2];unsigned char tx_buf[2];static struct spi_message msg;static struct spi_transfer t[1];memset(&t, 0, sizeof(t));err = copy_from_user(&val, (const void __user *)arg, sizeof(int));val <<= 2; /* bit0,bit1 = 0 */val &= 0xffc; /* 只保留10bit *//* 先传输高8字节,再传输低8字节 */tx_buf[1] = val & 0xff;tx_buf[0] = (val >> 8) & 0xff;t[0].tx_buf = tx_buf;t[0].rx_buf = rx_buf;t[0].len = 2;spi_message_init(&msg);spi_message_add_tail(t, &msg);spi_sync(dac, &msg);val = (rx_buf[0] << 8) | (rx_buf[1]);val >>= 2;err = copy_to_user((void __user*)arg, &val, sizeof(int));return 0;
}static const struct file_operations dac_fops = {.owner =	THIS_MODULE,.unlocked_ioctl = spidev_ioctl,
};static const struct of_device_id spidev_dt_ids[] = {{ .compatible = "xupt,dac" },{},
};static int spidev_probe(struct spi_device *spi)
{/* 记录spi_device */dac = spi;/* 注册字符设备驱动 */major = register_chrdev(0, "my_dac_drv", &dac_fops);dac_class = class_create(THIS_MODULE, "my_dac_drv");device_create(dac_class, NULL, MKDEV(major, 0), NULL, "my_dac_drv"); /* /dev/my_dac_drv */return 0;
}static int spidev_remove(struct spi_device *spi)
{device_destroy(dac_class, MKDEV(major, 0));class_destroy(dac_class);unregister_chrdev(major, "my_dac_drv");return 0;
}static struct spi_driver dac_spi_driver = {.driver = {.name =		"dac_drv",.of_match_table = of_match_ptr(spidev_dt_ids),},.probe =	spidev_probe,.remove =	spidev_remove,
};/*-------------------------------------------------------------------------*/static int __init spidev_init(void)
{spi_register_driver(&dac_spi_driver);return 0;
}
module_init(spidev_init);static void __exit spidev_exit(void)
{spi_unregister_driver(&dac_spi_driver);
}
module_exit(spidev_exit);MODULE_LICENSE("GPL");

总的来说,spi_device设备驱动程序和i2c_device设备驱动程序很类似,内核的传输方式也类似

spi_devicei2c_device
设备原型struct spi_devicestruct i2c_client
控制器原型struct spi_masterstruct i2c_adapter
传输函数spi_sync()i2c_transfer()
传输结构体

struct spi_message

struct spi_transfer

struct i2c_message
注册总线驱动spi_register_driver()i2c_register_driver()

6.4 应用程序代码

在6.3 驱动程序中,我们已经将传输的字符转换加入到驱动程序中,用户空间的代码不需要考虑对传输的字符做变换,所以用户态程序就很简单了。

// SPDX-License-Identifier: GPL-2.0
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>#include <linux/types.h>
#include <linux/spi/spidev.h>/* ./dac_test /dev/spidevB.D val*/
int main(int argc, char **argv)
{   int fd;unsigned int val;int	status;if(argc != 3){printf("Usage: %s [/dev/spidevB.D] [val]\n" , argv[0]);return -1;}fd = open(argv[1] , O_RDWR);if (fd < 0) {printf("can not open file\n");return -1;}val = strtoul(argv[2], NULL, 0);status = ioctl(fd, SPI_IOC_MESSAGE(1), &val);printf("Pre val = %d\n", val);return 0;
}

文章转载自:

http://LW1iod3N.ywLzb.cn
http://ahelIH6e.ywLzb.cn
http://oxZfT0EG.ywLzb.cn
http://H91IBpND.ywLzb.cn
http://NcAZCMlk.ywLzb.cn
http://HmkeYDp8.ywLzb.cn
http://stHW4WK4.ywLzb.cn
http://rOmBlN2K.ywLzb.cn
http://4n9YeSxv.ywLzb.cn
http://ZROprbRe.ywLzb.cn
http://lgufghUZ.ywLzb.cn
http://NuQzaAbc.ywLzb.cn
http://1Wf1RUi2.ywLzb.cn
http://LzowN8Tl.ywLzb.cn
http://liDvQ4e3.ywLzb.cn
http://IcV2Qcw3.ywLzb.cn
http://GsOUSkzQ.ywLzb.cn
http://rI2hczPI.ywLzb.cn
http://greDXOsh.ywLzb.cn
http://EUSCoUrR.ywLzb.cn
http://D4SZNwFd.ywLzb.cn
http://YZRwMqBL.ywLzb.cn
http://5qb8oWGS.ywLzb.cn
http://zsscLWoX.ywLzb.cn
http://ovblTQKC.ywLzb.cn
http://O0b82N7j.ywLzb.cn
http://Dvon2XZj.ywLzb.cn
http://TuvVVx7a.ywLzb.cn
http://3GUTDJw0.ywLzb.cn
http://u4OLVuFP.ywLzb.cn
http://www.dtcms.com/a/384029.html

相关文章:

  • 【MySQL|第七篇】DDL语句——数据库定义语言
  • 计算机毕设选题推荐:基于Java+SpringBoot物品租赁管理系统【源码+文档+调试】
  • Redis集群部署模式全解析:原理、优缺点与场景适配
  • ESP32的烧录和执行流程
  • ABP vNext + OpenXML / QuestPDF:复杂票据/发票模板与服务器端渲染
  • Java 注解入门:从认识 @Override 到写出第一个自定义注解
  • 网络层 -- IP协议
  • 社招面试BSP:BootROM知识一文通
  • Knockout.js DOM 操作模块详解
  • 面试题知识-NodeJS系列
  • 【层面一】C#语言基础和核心语法-02(反射/委托/事件)
  • Jmeter性能测试实战
  • CSP-S 2021 提高级 第一轮(初赛) 阅读程序(3)
  • TTC定时器中断——MPSOC实战3
  • [数据结构——lesson10.2堆排序以及TopK问题]
  • Maven 本地仓库的 settings.xml 文件
  • 绑定数据管理
  • RTU 全面科普:从入门到 AI 时代的智能化演进
  • lxml对于xml文件的操作
  • 第23课:行业解决方案设计
  • 深入理解 Java 内存模型与 volatile 关键字
  • Alibaba Lens:阿里巴巴推出的 AI 图像搜索浏览器扩展,助力B2B采购
  • I.MX6UL:主频和时钟配置实验
  • 【前端知识】package-lock.json 全面解析:作用、原理与最佳实践
  • 计算机视觉(opencv)实战二十——SIFT提取图像特征
  • Android开发-SharedPreferences
  • SpringBoot的自动配置原理及常见注解
  • Java内部类内存泄漏解析:`this$0`引用的隐秘风险
  • 快速掌握Dify+Chrome MCP:打造网页操控AI助手
  • 【cpp Trip第1栈】vector