Linux驱动-SPI-Linux中通用SPI设备驱动-应用程序调用

Linux驱动-SPI-Linux中通用SPI设备驱动-应用程序调用 提示Linux驱动-SPI-Linux中通用SPI设备驱动文章目录前言一、参考资料二、内核和设备树配置配置系统支持SPI接口配置SPI设备树三、spidev_test工具编译工具spidev_test工具spidev_test 使用命令参数说明读取设备信息写入和读取数据回环测试-./spidev_test -D /dev/spidev0.0 -l -vspidev_fdx 工具的使用命令参数说明三、应用程序如何使用SPI用户头文件spidev.h源码SPI 模式宏客户端配置 SPI 硬件时序附加功能标志可以用 | 或运算叠加到 modeioctl 魔数核心结构体 struct spi_ioc_transfer写客户端最重要其余 ioctl 命令设置 / 读取设备默认参数参考资料客户端程序编写初始化操作-基本设置传输操作命令控制实验总结前言前面的SPI我们从0开始编写了一个 mcp2515的驱动程序 而跟I2C 设备类似 在Linux内核中也有着通用 SPI 设备驱动 在这里将会了解通用 SPI 设备驱动的使用 并讲解如何在应用程序中通过 ioctl 对 SPI 进行配置和使用一、参考资料Linux驱动-在用户态控制I2C-两种方案ioctl和文件控制RK3568驱动指南第十六篇 SPI-第193章 Linux中通用SPI设备驱动二、内核和设备树配置之前我们自己写了SPI驱动其实系统里面都给我们写好了我们拿着直接用只需要在设备树中进行配置好即可。首先第一步就是去配置一次配置系统支持SPI接口通用 SPI 设备驱动在迅为提供的 Linux 内核中默认已经勾选了 具体路径如下所示配置SPI设备树除了内核支持之外 还需要修改设备树 由于之前已经使能了 SPI0 所以这直接修改之前编写的 mcp2515 设备树节点 具体设备树为“kernel/arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10.dtsi” 修改完成的 mcp2515 节点如下所示rockchip,spidev设备树修改如下compatible rockchip,spidevspi0{statusokay;pinctrl-0spi0m1_cs0spi0m1_pins;pinctrl-1spi0m1_cs0spi0m1_pins_hs;mcp2515:mcp25150{//compatible my-mcp2515;compatiblerockchip,spidev;reg0;spi-max-frequency1000000;statusokay;spi-cpol0;spi-cpha0;};};问题为什么设置compatible 为 rockchip,spidev在spi 驱动目录搜索如下那么基本可以确定系统自带的spi 驱动 匹配的就是rockchip,spidevtopeetubuntu:~/wfc/RK3568Linux/rk356x_linux/kernel/drivers/spi$ grep-rnrockchip,spidevspidev.c:680:{.compatiblerockchip,spidev},topeetubuntu:~/wfc/RK3568Linux/rk356x_linux/kernel/drivers/spi$编译内核重新刷机开发板启动之后 如果存在/dev/spidev0.0 设备节点 证明设备树及内核配置正确 如下图所示相关验证如下 参考之前Linux驱动-SPI外设代码框架编写 - 节点验证章节说明配置成功了设备树配置后对应的系统下面已经检测到相关设备节点了。[roottopeet:/]# ls/dev/spidev0.0/dev/spidev0.0/dev/spidev0.0 表示一个 SPI 总线上的具体设备。 0.0 是一个标识符 用于区分系统中的不同 SPI 控制器和设备。 这个标识符由两部分组成第一个数字 0 表示 SPI 总线的编号。 一个系统中可能有多个 SPI 控制器 每个控制器对应一个总线编号 从 0 开始。第二个数字 0 表示连接在该 SPI 总线上的具体设备编号。 一个 SPI 总线上可以连接多个设备 每个设备通过片选信号Chip Select, CS 进行区分 设备编号从 0 开始。三、spidev_test工具编译工具spidev_testspidev_test 是一个用于测试和调试 SPI 设备的命令行工具 通常在 Linux 系统上使用 它允许用户直接通过 SPI 总线与设备进行通信 可以发送数据并接收来自设备的响应。spidev_test 源码位于 Linux 源码的 kernel/tools/spi 目录下 如下图所示makeCC/home/topeet/wfc/RK3568Linux/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-gccLD/home/topeet/wfc/RK3568Linux/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-ld工具spidev_test 使用spidev_test是内核源码自带的SPI 用户态测试工具用来直接操作/dev/spidevX.Y设备不写内核驱动在应用层测试 SPI 硬件是否正常。命令参数说明参数长参数含义实操示例-D–device指定 SPI 设备节点必选-D /dev/spidev0.0-s–speedSPI 时钟频率 (Hz)-s 10000000 10MHz-d–delay传输后延时单位微秒 us-d 10 传输完等待 10us-b–bpw每个 word 多少 bitMCP2515 固定 8bit-b 8-i–input从文件读取要发送的数据-i send.bin-o–output把 SPI 接收到的数据保存到文件-o recv.bin-l–loop回环模式主板 MOSI 直接接 MISO 自测不接外设-l-H–cphaCPHA1时钟第二边沿采样MCP2515不要加-O–cpolCPOL1时钟空闲高电平MCP2515不要加-L–lsb低位先行SPI 绝大多数芯片是高位先行 MSB不要加不用-C–cs‑high片选 CS 高电平有效SPI 外设大多 CS 低有效不要加不用-3–3wire三线 SPIMOSI/MISO 共用一根线不用-v–verbose打印 tx/rx 缓冲区完整十六进制调试必开-v-p无传入要发送的字节数据老版本代替 - t-p “\x03\x0e”-N–no‑cs不控制片选 CS由外部自己控制 CSMCP2515 不要加不用-R–ready硬件 ready 引脚暂停传输很少用不用-2–dualdual‑SPI 双线输出普通 SPI 不用不用-4–quadquad‑SPI 四线flash 用普通 SPI 不用不用-S–size传输字节总数如果 -p 给数据可以不用手动指定-I–iter循环执行多少次传输-I 5 循环跑 5 次读取设备信息命令spidev_test -D /dev/spidevX.Y -s 1000000[roottopeet:/mnt/sdcard]# ./spidev_test -D /dev/spidev0.0 -s 1000000 spi mode: 0x0 bits per word: 8 max speed: 1000000 Hz (1000 KHz)这会使用 1 MHz 的时钟频率从 SPI 设备读取数据 默认情况下以十六进制显示。写入和读取数据命令spidev_test -D /dev/spidevX.Y -s 1000000 -b 8 -d 1000 -H -p hello[roottopeet:/mnt/sdcard]#./spidev_test-D/dev/spidev0.0-s1000000-b8-d1000-H-phellospi mode:0x1bits per word:8max speed:1000000Hz(1000KHz)[roottopeet:/mnt/sdcard]#[roottopeet:/mnt/sdcard]#这条命令会向 SPI 设备写入字符串 ‘hello’ 并以十六进制模式显示设备的响应数据。 -b 8。指定每个字的位数为 8 -d 1000 设置 1000 微秒的延迟。回环测试-./spidev_test -D /dev/spidev0.0 -l -v将开发板底板MISO-MOSI 连接在一起进行回环测试验证。验证 TX 、RX 数据自发自收一样数据 来验证 SPI 配置是否有问题没问题就可以进行外设接入了。spidev_fdx 工具的使用pidev_fdxspidev full‑duplexSPI 全双工测试工具也是内核tools/spi/下面的测试程序和spidev_test在一起。区别spidev_test功能多支持文件输入输出、回环、各种 SPI 模式spidev_fdx极简版只做一件事全双工收发指定长度字节代码很短专门用来快速做全双工读写没有复杂附加功能。基本介绍 spidev_fdx 是一个用于全双工 SPI 通信测试的命令行工具 主要用于在Linux 系统上与 SPI 设备进行双向数据传输和测试。命令参数说明参数说明devSPI 设备节点 /dev/spidev0.0speedSPI 时钟频率单位 Hzbpwbits‑per‑wordMCP2515 固定写8len要收发多少字节全双工发 len 字节同时收 len 字节三、应用程序如何使用SPI回顾我们之前学习SPi 内容知识点包括I2C 学习中我们是先自己写了I2C / SPI 驱动然后自己写了 用户态的控制程序app。其实 硬件I2C 也好、硬件SPI也罢 系统都提供了驱动程序拿着直接用就可以了。用户只需要自己写自己的控制程序 依托于系统提供的驱动程序今夕用户态和内核态进行通讯控制。用户头文件spidev.h在应用程序中可以通过 ioctl 来获取和配置 SPI 的相关属性 并实现 SPI 数据的发送和接收SPI 的 ioctl 宏定义在/usr/include/linux/spi/spidev.h定位用户空间客户端程序专用头文件写应用程序操作/dev/spidevX.Y就 include 这个头。内核驱动代码禁止包含此头内核用linux/spi/spi.h。源码/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note *//* * include/linux/spi/spidev.h * * Copyright (C) 2006 SWAPP * Andrea Paterniani a.paternianiswapp-eng.it * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */#ifndefSPIDEV_H#defineSPIDEV_H#includelinux/types.h#includelinux/ioctl.h/* User space versions of kernel symbols for SPI clocking modes, * matching linux/spi/spi.h */#defineSPI_CPHA0x01#defineSPI_CPOL0x02#defineSPI_MODE_0(0|0)#defineSPI_MODE_1(0|SPI_CPHA)#defineSPI_MODE_2(SPI_CPOL|0)#defineSPI_MODE_3(SPI_CPOL|SPI_CPHA)#defineSPI_CS_HIGH0x04#defineSPI_LSB_FIRST0x08#defineSPI_3WIRE0x10#defineSPI_LOOP0x20#defineSPI_NO_CS0x40#defineSPI_READY0x80#defineSPI_TX_DUAL0x100#defineSPI_TX_QUAD0x200#defineSPI_RX_DUAL0x400#defineSPI_RX_QUAD0x800/*---------------------------------------------------------------------------*//* IOCTL commands */#defineSPI_IOC_MAGICk/** * struct spi_ioc_transfer - describes a single SPI transfer * tx_buf: Holds pointer to userspace buffer with transmit data, or null. * If no data is provided, zeroes are shifted out. * rx_buf: Holds pointer to userspace buffer for receive data, or null. * len: Length of tx and rx buffers, in bytes. * speed_hz: Temporary override of the devices bitrate. * bits_per_word: Temporary override of the devices wordsize. * delay_usecs: If nonzero, how long to delay after the last bit transfer * before optionally deselecting the device before the next transfer. * cs_change: True to deselect device before starting the next transfer. * word_delay_usecs: If nonzero, how long to wait between words within one * transfer. This property needs explicit support in the SPI controller, * otherwise it is silently ignored. * * This structure is mapped directly to the kernel spi_transfer structure; * the fields have the same meanings, except of course that the pointers * are in a different address space (and may be of different sizes in some * cases, such as 32-bit i386 userspace over a 64-bit x86_64 kernel). * Zero-initialize the structure, including currently unused fields, to * accommodate potential future updates. * * SPI_IOC_MESSAGE gives userspace the equivalent of kernel spi_sync(). * Pass it an array of related transfers, theyll execute together. * Each transfer may be half duplex (either direction) or full duplex. * * struct spi_ioc_transfer mesg[4]; * ... * status ioctl(fd, SPI_IOC_MESSAGE(4), mesg); * * So for example one transfer might send a nine bit command (right aligned * in a 16-bit word), the next could read a block of 8-bit data before * terminating that command by temporarily deselecting the chip; the next * could send a different nine bit command (re-selecting the chip), and the * last transfer might write some register values. */struct spi_ioc_transfer{__u64 tx_buf;__u64 rx_buf;__u32 len;__u32 speed_hz;__u16 delay_usecs;__u8 bits_per_word;__u8 cs_change;__u8 tx_nbits;__u8 rx_nbits;__u8 word_delay_usecs;__u8 pad;/* If the contents of struct spi_ioc_transfer ever change * incompatibly, then the ioctl number (currently 0) must change; * ioctls with constant size fields get a bit more in the way of * error checking than ones (like this) where that field varies. * * NOTE: struct layout is the same in 64bit and 32bit userspace. */};/* not all platforms use asm-generic/ioctl.h or _IOC_TYPECHECK() ... */#defineSPI_MSGSIZE(N)\((((N)*(sizeof(struct spi_ioc_transfer)))(1_IOC_SIZEBITS))\?((N)*(sizeof(struct spi_ioc_transfer))):0)#defineSPI_IOC_MESSAGE(N)_IOW(SPI_IOC_MAGIC,0,char[SPI_MSGSIZE(N)])/* Read / Write of SPI mode (SPI_MODE_0..SPI_MODE_3) (limited to 8 bits) */#defineSPI_IOC_RD_MODE_IOR(SPI_IOC_MAGIC,1,__u8)#defineSPI_IOC_WR_MODE_IOW(SPI_IOC_MAGIC,1,__u8)/* Read / Write SPI bit justification */#defineSPI_IOC_RD_LSB_FIRST_IOR(SPI_IOC_MAGIC,2,__u8)#defineSPI_IOC_WR_LSB_FIRST_IOW(SPI_IOC_MAGIC,2,__u8)/* Read / Write SPI device word length (1..N) */#defineSPI_IOC_RD_BITS_PER_WORD_IOR(SPI_IOC_MAGIC,3,__u8)#defineSPI_IOC_WR_BITS_PER_WORD_IOW(SPI_IOC_MAGIC,3,__u8)/* Read / Write SPI device default max speed hz */#defineSPI_IOC_RD_MAX_SPEED_HZ_IOR(SPI_IOC_MAGIC,4,__u32)#defineSPI_IOC_WR_MAX_SPEED_HZ_IOW(SPI_IOC_MAGIC,4,__u32)/* Read / Write of the SPI mode field */#defineSPI_IOC_RD_MODE32_IOR(SPI_IOC_MAGIC,5,__u32)#defineSPI_IOC_WR_MODE32_IOW(SPI_IOC_MAGIC,5,__u32)#endif/* SPIDEV_H */SPI 模式宏客户端配置 SPI 硬件时序#defineSPI_CPHA0x01#defineSPI_CPOL0x02#defineSPI_MODE_0(0|0)// CPOL0 CPHA0 → MCP2515必须用这个#defineSPI_MODE_1(0|SPI_CPHA)// CPOL0 CPHA1#defineSPI_MODE_2(SPI_CPOL|0)// CPOL1 CPHA0#defineSPI_MODE_3(SPI_CPOL|SPI_CPHA)// CPOL1 CPHA1SPI_CPOL时钟极性总线空闲的时候 SCK 电平SPI_CPHA时钟相位在哪一个边沿采样数据附加功能标志可以用|或运算叠加到 mode#defineSPI_CS_HIGH0x04// CS片选高电平有效默认低电平有效#defineSPI_LSB_FIRST0x08// 低位bit先发MCP2515高位先发不要开#defineSPI_3WIRE0x10// 三线SPI MOSI/MISO共用一根线#defineSPI_LOOP0x20// 内部回环测试MOSI接MISO自测用#defineSPI_NO_CS0x40// 不自动控制CSMCP2515绝对不能开启#defineSPI_READY0x80// 从设备就绪引脚很少用#defineSPI_TX_DUAL0x100#defineSPI_TX_QUAD0x200#defineSPI_RX_DUAL0x400#defineSPI_RX_QUAD0x800ioctl 魔数#defineSPI_IOC_MAGICkioctl 魔术号k内核 spidev 驱动识别这个魔数知道这是 spidev 设备的指令。核心结构体struct spi_ioc_transfer写客户端最重要其余 ioctl 命令设置 / 读取设备默认参数区分两个概念下面这一组 ioctl修改设备 fd 的默认参数struct spi_ioc_transfer内部speed_hz、bits_per_word单次传输临时覆盖不修改默认参数。// 读写8位mode只支持低8位标志不支持Dual/Quad#defineSPI_IOC_RD_MODE_IOR(SPI_IOC_MAGIC,1,__u8)#defineSPI_IOC_WR_MODE_IOW(SPI_IOC_MAGIC,1,__u8)// 读写LSB_FIRST低位先行#defineSPI_IOC_RD_LSB_FIRST_IOR(SPI_IOC_MAGIC,2,__u8)#defineSPI_IOC_WR_LSB_FIRST_IOW(SPI_IOC_MAGIC,2,__u8)// 读写默认每个word比特数#defineSPI_IOC_RD_BITS_PER_WORD_IOR(SPI_IOC_MAGIC,3,__u8)#defineSPI_IOC_WR_BITS_PER_WORD_IOW(SPI_IOC_MAGIC,3,__u8)// 读写设备默认最大时钟频率#defineSPI_IOC_RD_MAX_SPEED_HZ_IOR(SPI_IOC_MAGIC,4,__u32)#defineSPI_IOC_WR_MAX_SPEED_HZ_IOW(SPI_IOC_MAGIC,4,__u32)// ✅推荐使用32位mode完整容纳全部SPI标志位#defineSPI_IOC_RD_MODE32_IOR(SPI_IOC_MAGIC,5,__u32)#defineSPI_IOC_WR_MODE32_IOW(SPI_IOC_MAGIC,5,__u32)参考资料假若我们要写客户端程序时候那么可以参考系统自带的一些程序上面我们不是了解研究过spi 工具嘛那么直接看他们的源码即可。客户端程序编写初始化操作-基本设置intfd;// SPI设备文件描述符intmodeSPI_MODE_0;// SPI模式intbits8;// 每字比特数intspeed10000000;// 最大SPI总线速度Hz/* * 初始化SPI通信. * 返回0表示成功-1表示失败. */intspi_init(void){intret;// 打开SPI设备文件fdopen(/dev/spidev0.0,O_RDWR);if(fd0){printf(打开 /dev/spidev0.0 错误\n);return-1;}/* * 设置SPI模式 */retioctl(fd,SPI_IOC_WR_MODE32,mode);if(ret-1)printf(无法设置SPI模式\n);retioctl(fd,SPI_IOC_RD_MODE32,mode);if(ret-1)printf(无法获取SPI模式\n);/* * 设置每字比特数 */retioctl(fd,SPI_IOC_WR_BITS_PER_WORD,bits);if(ret-1)printf(无法设置每字比特数\n);retioctl(fd,SPI_IOC_RD_BITS_PER_WORD,bits);if(ret-1)printf(无法获取每字比特数\n);/* * 设置最大传输速度 */retioctl(fd,SPI_IOC_WR_MAX_SPEED_HZ,speed);if(ret-1)printf(无法设置最大传输速度\n);retioctl(fd,SPI_IOC_RD_MAX_SPEED_HZ,speed);if(ret-1)printf(无法获取最大传输速度\n);printf(SPI模式: 0x%x\n,mode);printf(每字比特数: %d\n,bits);printf(最大速度: %d Hz (%d KHz)\n,speed,speed/1000);return0;}传输操作/* * 执行SPI数据传输. * 参数: * fd - SPI设备文件描述符 * tx - 发送缓冲区 * rx - 接收缓冲区 * len - 数据长度 * 返回0表示成功-1表示失败. */inttransfer(intfd,char*tx,char*rx,intlen){intret;struct spi_ioc_transfer tr{.tx_buf(unsignedlong)tx,.rx_buf(unsignedlong)rx,.lenlen,.delay_usecsdelay,.speed_hzspeed,.bits_per_wordbits,};retioctl(fd,SPI_IOC_MESSAGE(1),tr);if(ret1){printf(无法发送SPI消息\n);return-1;}return0;}命令控制实验intmain(intargc,char*argv[]){charreset_cmd[1]{RESET};// 复位命令数组charrd_canstat[2]{READ,CANSTAT};// 读CAN状态寄存器命令数组charcanstat[4]{0};// 存储CAN状态的缓冲区 为什么是4 》所以 写了两个数据 那么 回两个数据共4个数据。前三个数据对自己来说无用的所以 下面最后获取的是canstat[3]。charwr_canctrl[]{WRITE,CANCTRL,0x00};// 写CAN控制寄存器命令数组// 初始化SPI通信spi_init();// 执行SPI数据传输// 1. 发送复位命令transfer(fd,reset_cmd,NULL,sizeof(reset_cmd));// 2. 读取CAN状态transfer(fd,rd_canstat,canstat,sizeof(canstat));printf(CAN状态为 %x\n,canstat[2]);// 清空canstat缓冲区memset(canstat,0,sizeof(canstat));// 3. 写入CAN控制transfer(fd,wr_canctrl,NULL,sizeof(wr_canctrl));// 4. 再次读取CAN状态transfer(fd,rd_canstat,canstat,sizeof(canstat));printf(CAN状态为 %x\n,canstat[3]);return0;}查看实验结果总结实战项目中直接用系统提供的SPI 驱动然后自己写客户端程序进行控制对比I2C 控制程序其实一摸一样的玩法 直接用系统提供的接口