概念
通信模式可以分为单工、半双工和全双工,
单工通信指信号只在一个方向上传输,仅 能发送或接收,
而半双工通信指信号可以在俩个方向上传输,但某一个时刻只允许发送或接收,
而全双工通信指数据同时在俩个方向上传输,而 SPI 只需要4根信号线(SCLK、MOSI、MISO、CS)即可完成主从设备之间的数据交换,实现全双 工通信。减少了引脚数,简化了接线
通信原理:
本质就是进行数据交换,就是你发给我一个数据,我发给你一个数据这样,比如你只想读数据,你可以发OxFF这样,得到的是他给你的数据,你只想写,从机发给你0xFF这样
时钟和相位:
在SPI的通信之前需要先确定时钟信号的默认状态以及时钟信号的采样时间,这两个参数 由CPOL(时钟极性ClockPolarity)和 CPHA(时钟相位ClockPhase)来确定
1.CPOL(时钟极性ClockPolarity) CPOL 定义了时钟信号的默认状态(即空闲状态)。 CPOL =0 时,表示时钟信号在空闲状态下为低电平(0)。 CPOL =1 时,表示时钟信号在空闲状态下为高电平(1)。 2.CPHA(时钟相位ClockPhase) CPHA 定义了数据信号相对于时钟信号的采样时间。 CPHA=0 时,表示数据在时钟的第一个边沿(上升或下降)被采样。 CPHA=1 时,表示数据在时钟的第二个边沿(上升或下降)被采样。
RK3568的SPI介绍
·支持4个SPI控制器 ·1个控制器支持1个片选输出其余3个控制器各支持2个片选输出 ·支持主机模式和从机模式,可通过软件进行配置切换
SPI子系统框架
三个层的主要作用和I2C一样
修改设备树:
&spi0 { status = "okay"; pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>; pinctrl-1 = <&spi0m1_cs0 &spi0m1_pins_hs>; mcp2515: mcp2515@0 { compatible = "my-mcp2515"; //表示使用硬件片选0,频率 //如果软件片选的话 //cs-gpios=<>; reg = <0>; /* SPI片选号,0表示使用SPI0的CS0,对应设备树的片选引脚 */ //spi-cpha; /* SPI时钟相位配置,不写表示CPHA=0,第一个边沿采样数据 */ //spi-cpol; /* SPI时钟极性配置,不写表示CPOL=0,空闲时时钟为低电平 */ //spi-lsb-first; /* 数据传输格式,不写表示MSB先行(高位先发送),MCP2515必须用这个 */ //spi-cs-high; /* 片选有效电平,不写表示低电平有效,MCP2515默认低电平有效 */ reg = <0>; spi-max-frequency = <10000000>; status = "okay"; }; };SPI的驱动代码:
#include <linux/init.h> #include <linux/module.h> #include <linux/spi/spi.h> // MCP2515设备初始化函数 static int mcp2515_probe(struct spi_device *spi) { printk("This is mcp2515 probe\n"); return 0; } // MCP2515设备移除函数 static int mcp2515_remove(struct spi_device *spi) { return 0; } // MCP2515设备匹配表,用于设备树匹配 static const struct of_device_id mcp2515_of_match_table[] = { { .compatible = "my-mcp2515" }, { } }; // MCP2515设备ID匹配表,用于总线匹配 static const struct spi_device_id mcp2515_id_table[] = { { "mcp2515", 0 }, { } }; // MCP2515 SPI驱动结构体 static struct spi_driver spi_mcp2515 = { .probe = mcp2515_probe, // 探测函数 .remove = mcp2515_remove, // 移除函数 .driver = { .name = "mcp2515", // 驱动名称 .owner = THIS_MODULE, // 所属模块 .of_match_table = mcp2515_of_match_table, // 设备树匹配表 }, .id_table = mcp2515_id_table, // 设备ID匹配表 }; // 驱动初始化函数 static int __init mcp2515_init(void) { int ret; // 注册SPI驱动 ret = spi_register_driver(&spi_mcp2515); return ret; } // 驱动退出函数 static void __exit mcp2515_exit(void) { // 注销SPI驱动 spi_unregister_driver(&spi_mcp2515); } module_init(mcp2515_init); module_exit(mcp2515_exit); MODULE_LICENSE("GPL");注册字符设备代码:(和之前一样,可以不看)
#include <linux/init.h> #include <linux/module.h> #include <linux/spi/spi.h> #include <linux/cdev.h> #include <linux/fs.h> dev_t dev_num; // 设备号 struct cdev mcp2515_cdev; // 字符设备对象 struct class *mcp2515_class; // 设备类 struct device *mcp2515_dev; // 设备节点 struct spi_device *spi_global; // 保存spi设备 // 打开设备 int mcp2515_open(struct inode *inode, struct file *file) { return 0; } // 读设备 ssize_t mcp2515_read(struct file *file, char __user *buf, size_t size, loff_t *offset) { return 0; } // 写设备 ssize_t mcp2515_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) { return 0; } // 关闭设备 int mcp2515_release(struct inode *inode, struct file *file) { return 0; } // 文件操作集合 struct file_operations fops = { .open = mcp2515_open, .read = mcp2515_read, .write = mcp2515_write, .release = mcp2515_release, }; // 设备树匹配成功后进入probe int mcp2515_probe(struct spi_device *spi) { spi_global = spi; // 保存spi设备 // 1. 申请设备号 alloc_chrdev_region(&dev_num, 0, 1, "mcp2515"); // 2. 初始化字符设备 cdev_init(&mcp2515_cdev, &fops); mcp2515_cdev.owner = THIS_MODULE; cdev_add(&mcp2515_cdev, dev_num, 1); // 3. 创建类和设备节点 /dev/mcp2515 mcp2515_class = class_create(THIS_MODULE, "mcp2515"); mcp2515_dev = device_create(mcp2515_class, NULL, dev_num, NULL, "mcp2515"); printk("mcp2515 probe ok\n"); return 0; } // 移除设备 int mcp2515_remove(struct spi_device *spi) { // 销毁字符设备 device_destroy(mcp2515_class, dev_num); class_destroy(mcp2515_class); cdev_del(&mcp2515_cdev); unregister_chrdev_region(dev_num, 1); return 0; } // 设备树匹配表 static const struct of_device_id mcp2515_match[] = { { .compatible = "my-mcp2515" }, {}, }; // spi驱动id表 static const struct spi_device_id mcp2515_id[] = { { "mcp2515", 0 }, {}, }; // spi驱动结构体 struct spi_driver mcp2515_spi_driver = { .probe = mcp2515_probe, .remove = mcp2515_remove, .driver = { .name = "mcp2515", .of_match_table = mcp2515_match, }, .id_table = mcp2515_id, }; // 驱动入口 static int __init mcp2515_init(void) { spi_register_driver(&mcp2515_spi_driver); return 0; } // 驱动出口 static void __exit mcp2515_exit(void) { spi_unregister_driver(&mcp2515_spi_driver); } module_init(mcp2515_init); module_exit(mcp2515_exit); MODULE_LICENSE("GPL");SPI的API
/* SPI同步写函数:只发送数据,不接收数据,阻塞式传输 * spi : 指向SPI设备的指针(指定与哪个设备通信) * buf : 发送数据的缓冲区(存放要写入的数据) * len : 要发送的数据长度(单位:字节) * 返回值: 0成功,负数失败 */ static inline int spi_write(struct spi_device *spi, const void *buf, size_t len); /* SPI同步读函数:只接收数据,不发送数据,阻塞式传输 * spi : 指向SPI设备的指针 * buf : 接收数据的缓冲区(存放读取到的数据) * len : 要读取的数据长度(单位:字节) * 返回值: 0成功,负数失败 */ static inline int spi_read(struct spi_device *spi, void *buf, size_t len);MCP2515复位函数
MCP2515外设通信必须先发指令,再发数据,比如0XC0复位,0X02写寄存器
// MCP2515芯片复位函数 void mcp2515_reset(void) { int ret; char write_buf[] = {0xc0}; // 复位指令 0xC0 ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送复位命令 if (ret < 0) { printk("spi_write is error\n"); // 打印错误信息 } }读写寄存器函数:
/* SPI 先写后读函数:先发送数据,再读取数据,一条SPI总线完成 * spi : SPI设备指针 * tx_buf : 发送数据缓冲区 * tx_len : 发送数据长度(字节) * rx_buf : 接收数据缓冲区 * rx_len : 接收数据长度(字节) * 返回值 : 0成功,负数失败 */ static inline int spi_write_then_read(struct spi_device *spi, const void *tx_buf, unsigned int tx_len, void *rx_buf, unsigned int rx_len);/* 读取MCP2515寄存器的值 * reg : 要读取的寄存器地址 * 返回值: 读取到的寄存器数据 */ char mcp2515_read_reg(char reg) { char write_buf[] = {0x03, reg}; // 读指令0x03 + 寄存器地址 char read_buf; // 存放读取到的数据 int ret; // 先发送指令+地址,再读取1字节数据 ret = spi_write_then_read(spi_dev, write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf)); if (ret < 0) { printk("spi_write_then_read error\n"); return ret; } return read_buf; // 返回读到的数据 }部分代码如下:其他与上面一样,probe函数加上
// MCP2515 设备探测函数(设备树匹配成功后执行) int mcp2515_probe(struct spi_device *spi) { mcp2515_reset(); // 复位MCP2515设备 value = mcp2515_read_reg(0x0e); // 读取寄存器0x07的值 printk("value is %x\n", value); // 打印读取到的寄存器值 return 0; // 返回成功 }写寄存器函数:
// MCP2515写寄存器函数:向指定寄存器写入数据 void mcp2515_write_reg(char reg, char value) { int ret; // 写指令0x02 + 寄存器地址 + 要写入的值 char write_buf[] = {0x02, reg, value}; // 通过SPI发送完整数据 ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); if(ret < 0){ printk("mcp2515_write_reg error\n"); } }// MCP2515修改寄存器位函数(位修改指令:0x05) // reg: 要修改的寄存器地址 // mask: 位屏蔽字(决定哪些位可被修改) // value: 要写入的位数据 void mcp2515_change_regbit(char reg, char mask, char value) { int ret; // 写缓冲区:位修改指令0x05 + 寄存器地址 + 屏蔽码 + 数据 char write_buf[] = {0x05, reg, mask, value}; // 发送SPI数据 ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); if(ret < 0){ printk("mcp2515_change_regbit error\n"); } }部分代码如下:其他与上面一样,probe函数加上
//eg:0xe0 → 二进制 11100000表示:只改最高 3 位,其他位不动 //0x40 → 二进制 01000000表示:把那 3 位改成 010 // MCP2515 设备探测函数(设备树匹配成功后执行) int mcp2515_probe(struct spi_device *spi) { // 配置 MCP2515 寄存器(CAN 波特率 + 模式 + 中断 + 接收) mcp2515_write_reg(CNF1, 0x01); // 配置波特率寄存器1 mcp2515_write_reg(CNF2, 0xb1); // 配置波特率寄存器2 mcp2515_write_reg(CNF3, 0x05); // 配置波特率寄存器3 mcp2515_write_reg(RXB0CTRL, 0x60); // 配置接收缓冲器0 mcp2515_write_reg(CANINTE, 0x05); // 打开接收中断使能 // 修改 CANCTRL 寄存器的特定位(设置模式) mcp2515_change_regbit(CANCTRL, 0xe0, 0x40); // 读取寄存器并打印,验证配置是否成功 value = mcp2515_read_reg(0x0e); printk("value is %x\n", value); }设备节点的read,write函数
write函数
/* * 函数名称:mcp2515_write * 功能:应用层 write 时触发,用于 MCP2515 发送一帧 CAN 数据 * 参数: * file : 文件结构体 * buf : 用户空间数据缓冲区 * size : 要写入的数据长度 * offset : 文件偏移地址 * 返回值:成功返回写入长度,失败返回负数 */ size_t mcp2515_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) { char w_kbuf[13] = {0}; // 内核缓冲区,存储CAN帧数据 int ret; int i; // 配置发送控制器TXB0CTRL,使能发送相关位 mcp2515_change_regbit(TXB0CTRL, 0x03, 0x03); // 从用户空间拷贝数据到内核空间 ret = copy_from_user(w_kbuf, buf, size); if(ret) { printk("copy_from_user w_kbuf is error\n"); return -1; } // 将13字节CAN帧数据依次写入MCP2515发送寄存器(地址从0x31开始) for(i = 0; i < sizeof(w_kbuf); i++) { mcp2515_write_reg(0x31 + i, w_kbuf[i]); } // 启动发送:将TXB0CTRL的请求发送位拉高 mcp2515_change_regbit(TXB0CTRL, 0x08, 0x08); // 等待发送完成(查询CANINTF的发送完成标志位) while(!(mcp2515_read_reg(CANINTF) & (1 << 2))); // 清除发送完成标志位 mcp2515_change_regbit(CANINTF, 0x04, 0x00); return size; // 返回成功写入的长度 }read函数
#define CANINTF 0x2c // 读设备操作函数,从MCP2515读取一帧CAN数据到用户缓冲区 ssize_t mcp2515_read(struct file *file, char __user *buf, size_t size, loff_t *offset) { char r_kbuf[13] = {0}; // 内核缓冲区,存储从MCP2515读取的一帧CAN数据 int i; int ret; // 等待接收完成:检测CANINTF的bit0(接收中断标志) while(!(mcp2515_read_reg(CANINTF) & (1 << 0))); // 从MCP2515接收缓冲区 RXB0 读取13字节数据 for(i = 0; i < sizeof(r_kbuf); i++){ r_kbuf[i] = mcp2515_read_reg(0x61 + i); } // 清除接收完成标志位 mcp2515_change_regbit(CANINTF, 0x01, 0x00); // 将内核数据复制到用户空间 ret = copy_to_user(buf, r_kbuf, size); if(ret){ printk("copy_to_user r_kbuf is error\n"); return -1; } return 0; }测试APP
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> // 主函数,程序入口点 int main(int argc, char *argv[]) { int fd; // 文件描述符 int i; // 循环变量 // 写缓冲区,13字节的CAN数据帧 char w_buf[13] = {0x66, 0x08, 0x22, 0x33, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; // 读缓冲区,存放读取到的CAN数据 char r_buf[13] = {0}; // 打开设备 /dev/mcp2515,读写方式打开 fd = open("/dev/mcp2515", O_RDWR); if (fd < 0) { printf("open /dev/mcp2515 error\n"); return -1; } // 发送数据到CAN驱动 write(fd, w_buf, sizeof(w_buf)); // 读取CAN接收到的数据 read(fd, r_buf, sizeof(r_buf)); // 打印读取到的数据 for (i = 0; i < 13; i++) { printf("r_buf[%d] is %x\n", i, r_buf[i]); } // 关闭设备 close(fd); return 0; }SPI设备驱动的使用,生成设备节点(修改compatitle)
修改设备树,(就是compatitle)
&spi0 { status = "okay"; pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>; pinctrl-1 = <&spi0m1_cs0 &spi0m1_pins_hs>; mcp2515: mcp2515@0 { compatible = "rockchip,spidev"; reg = <0>; spi-max-frequency = <10000000>; status = "okay"; }; };spidev_test 工具使用
make cc=你的交叉工具链路径 LD=你的连接路径写入和读取数据:
spidev_test-D /dev/spidevX.Y-s 1000000-b 8-d 1000-H-p 'hello'这条命令会向 SPI 设备写入字符串 'hello',并以十六进制模式显示设备的响应数据。-b 8 指定每个字的位数为 8,-d 1000 设置 1000 微秒的延迟。
回环测试,没问题就可以挂载外设了
./spidev_test -D /dev/spidev0.0 -l -v应用程序如何使用spi
首先肯定也是ioctl命令来交互数据,一些宏定义如下
/* 读取 / 写入 SPI 模式(SPI_MODE_0 ~ SPI_MODE_3)(限制为 8 位) */ #define SPI_IOC_RD_MODE _IOR(SPI_IOC_MAGIC, 1, __u8) // 读取 SPI 模式 #define SPI_IOC_WR_MODE _IOW(SPI_IOC_MAGIC, 1, __u8) // 写入 SPI 模式 /* 读取 / 写入 SPI 位顺序 */ #define SPI_IOC_RD_LSB_FIRST _IOR(SPI_IOC_MAGIC, 2, __u8) // 读取 SPI 低位优先 #define SPI_IOC_WR_LSB_FIRST _IOW(SPI_IOC_MAGIC, 2, __u8) // 写入 SPI 低位优先 /* 读取 / 写入 SPI 设备字长(1 ~ N) */ #define SPI_IOC_RD_BITS_PER_WORD _IOR(SPI_IOC_MAGIC, 3, __u8) // 读取 SPI 每字位数 #define SPI_IOC_WR_BITS_PER_WORD _IOW(SPI_IOC_MAGIC, 3, __u8) // 写入 SPI 每字位数 /* 读取 / 写入 SPI 设备默认最大速度(Hz) */ #define SPI_IOC_RD_MAX_SPEED_HZ _IOR(SPI_IOC_MAGIC, 4, __u32) // 读取 SPI 最大速度(Hz) #define SPI_IOC_WR_MAX_SPEED_HZ _IOW(SPI_IOC_MAGIC, 4, __u32) // 写入 SPI 最大速度(Hz) /* 读取 / 写入 SPI 模式字段 */ #define SPI_IOC_RD_MODE32 _IOR(SPI_IOC_MAGIC, 5, __u32) // 读取 SPI 模式(32 位) #define SPI_IOC_WR_MODE32 _IOW(SPI_IOC_MAGIC, 5, __u32) // 写入 SPI 模式(32 位)用户代码如下
都是固定代码;别utransfer和配置模式等都是不变的代码
#include<stdio.h> #include<sys/ioctl.h> #include<linux/spi/spidev.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<string.h> #define RESET 0xc0 // 复位命令 #define CANSTAT 0x0e // CAN 状态寄存器地址 #define READ 0x03 // 读命令 #define CANCTRL 0x0f // CAN 控制寄存器地址 #define WRITE 0x02 // 写命令 int fd; // SPI 设备文件描述符 int mode = SPI_MODE_0; // SPI 模式 int bits = 8; // 每字比特数 int speed = 10000000; // 最大 SPI 总线速度(Hz) int delay; // 延迟时间(微秒) /* * 初始化 SPI 通信 * 返回 0 表示成功,-1 表示失败 */ int spi_init(void) { int ret; // 打开 SPI 设备文件 fd = open("/dev/spidev0.0", O_RDWR); /* 设置 SPI 模式 */ ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode); ret = ioctl(fd, SPI_IOC_RD_MODE32, &mode); /* 设置每字比特数 */ ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits); ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits); /* 设置最大传输速度 */ ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed); printf("SPI 模式: 0x%x\n", mode); printf("每字比特数: %d\n", bits); printf("最大速度: %d Hz (%d KHz)\n", speed, speed / 1000); return 0; } /* * 执行 SPI 数据传输 * 参数: * fd - SPI 设备文件描述符 * tx - 发送缓冲区 * rx - 接收缓冲区 * len - 数据长度 * 返回 0 表示成功,-1 表示失败 */ int transfer(int fd, char *tx, char *rx, int len) { int ret; struct spi_ioc_transfer tr = { .tx_buf = (unsigned long)tx, .rx_buf = (unsigned long)rx, .len = len, .delay_usecs = delay, .speed_hz = speed, .bits_per_word = bits, }; ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr); if (ret < 1) { printf("无法发送 SPI 消息\n"); return -1; } return 0; } int main(int argc, char *argv[]) { char reset_cmd[1] = {RESET}; // 复位命令数组 char rd_canstat[2] = {READ, CANSTAT}; // 读 CAN 状态寄存器 char canstat[3] = {0}; // 存储 CAN 状态的缓冲区 char wr_canctrl[3] = {WRITE, CANCTRL, 0x00}; // 写 CAN 控制寄存器 // 初始化 SPI 通信 spi_init(); // 1. 发送复位命令 transfer(fd, reset_cmd, NULL, sizeof(reset_cmd)); // 2. 读取 CAN 状态 transfer(fd, rd_canstat, canstat, sizeof(canstat)); printf("CAN 状态为: %x\n", canstat[2]); // 清空缓冲区 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[2]); close(fd); return 0; }软件模拟SPI
首先开启内核驱动
修改设备树:
compatitle改成下面这个,目的是匹配驱动
spi5: spi@gpiol { compatible = "spi-gpio"; #address-cells = <1>; sck-gpio = <&gpio0 RK_PB0 GPIO_ACTIVE_LOW>; miso-gpio = <&gpio1 RK_PB0 GPIO_ACTIVE_LOW>; mosi-gpio = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>; cs-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_LOW>; num-chipselects = <1>; pinctrl-names = "default"; pinctrl-0 = <&spi5_gpios>; status = "disabled"; };pinctrl添加下面节点:
spi5_gpios: gpios { rockchip,pins = <0 RK_PB0 0 &pcfg_pull_none>, <1 RK_PB0 0 &pcfg_pull_none>, <1 RK_PB1 0 &pcfg_pull_none>, <1 RK_PB2 0 &pcfg_pull_none>; };spi下面挂载设备:
&spi5 { status = "okay"; mcp2515: mcp2515@0 { compatible = "rockchip,spidev"; reg = <0>; spi-max-frequency = <10000000>; status = "okay"; }; };然后回环测试即可
移植MCP2515驱动
1:首先把驱动编译内核
2:修改设备树,描述硬件信息
&spi0 { status = "okay"; pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>; pinctrl-1 = <&spi0m1_cs0 &spi0m1_pins_hs>; mcp2515: mcp2515@0 { compatible = "microchip,mcp2515"; reg = <0>; spi-max-frequency = <10000000>; //下面是移植别人驱动要添加的引脚配置 pinctrl-names = "default"; pinctrl-0 = <&mcp2515_int>; interrupt-parent = <&gpio0>; interrupts = <RK_PB0 IRQ_TYPE_EDGE_FALLING>; clocks = <&clk8m>; status = "okay"; }; clk8m: clk8m { compatible = "fixed-clock"; #clock-cells = <0>; clock-frequency = <8000000>; }; };pinctrl添加节点:
mcp2515-gpio { mcp2515_int: mcp2515-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; }; };